WT 1.1.20 Training Plan improvements, InApp Messageing

This commit is contained in:
bossanyit 2021-06-25 17:30:14 +02:00
parent a595ffb358
commit 211307e63e
36 changed files with 693 additions and 300 deletions

BIN
asset/menu/FG_2_edz.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
asset/menu/stretching.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
asset/menu/warmup.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -436,7 +436,7 @@
"Do you want to restart, or select a new Training Plan?": "Do you want to restart, or select a new Training Plan?",
"New Training Plan": "New Training Plan",
"Restart": "Restart",
"Training Day": "Training Day",
"Training Day": "Day",
"No Active Training Plan": "No Active Training Plan",
"Please select one in the Training menu, or create your custom plan": "Please select one in the Training menu, or create your custom plan",
"Continue your training": "Continue your training",
@ -473,5 +473,8 @@
"A new version of Workout Test is available!": "A new version of Workout Test is available!",
"Update Now": "Update Now",
"Update App?": "Update App?",
"Want to update?": "Please update the app"
"Want to update?": "Please update the app",
"Attention!": "Attention!",
"The safe and exact execution of this exercise you need a training buddy or a trainer": "The safe and exact execution of this exercise you need a training buddy or a trainer",
"Execution at your own risk!": "Execution at your own risk!"
}

View File

@ -434,7 +434,7 @@
"Do you want to restart, or select a new Training Plan?": "Újra akarod indítani, vagy inkább egy másikat választasz?",
"New Training Plan": "Másik edzésterv",
"Restart": "Újraindítom",
"Training Day": "Edzésnap",
"Training Day": "Nap",
"No Active Training Plan": "Nincs aktív edzésterv",
"Please select one in the Training menu, or create your custom plan": "Kérlek válassz egyet a Tréning menüben, vagy hozd létre a saját egyéni edzésedet",
"Continue your training": "Folytasd az edzést",
@ -471,5 +471,8 @@
"A new version of Workout Test is available!": "Egy új Workout Test verzió elérhető!",
"Update Now": "Töltsd le most",
"Update App?": "App frissítés",
"Want to update?": "Kérlek töltsd le"
"Want to update?": "Kérlek töltsd le",
"Attention!": "Figyelem!",
"The safe and exact execution of this exercise you need a training buddy or a trainer": "A gyakorlat biztonságos és maximális pontosságú végrehajtásához edzőtárs vagy edző segítsége szükséges",
"Execution at your own risk!": "Végrehajtás CSAK saját felelőségre!"
}

View File

@ -4,7 +4,7 @@ PODS:
- AppAuth/ExternalUserAgent (= 1.4.0)
- AppAuth/Core (1.4.0)
- AppAuth/ExternalUserAgent (1.4.0)
- apple_sign_in (0.0.1):
- awesome_notifications (0.0.2):
- Flutter
- device_info (0.0.1):
- Flutter
@ -116,10 +116,10 @@ PODS:
- FirebaseInstallations (~> 8.0)
- GoogleUtilities/Environment (~> 7.4)
- "GoogleUtilities/NSData+zlib (~> 7.4)"
- flurry (0.0.4):
- Flurry-iOS-SDK/FlurrySDK (11.2.1)
- flurry_data (0.0.1):
- Flurry-iOS-SDK/FlurrySDK
- Flutter
- Flurry-iOS-SDK/FlurrySDK (11.2.1)
- Flutter (1.0.0)
- flutter_app_badger (0.0.1):
- Flutter
@ -127,8 +127,6 @@ PODS:
- FBSDKCoreKit (~> 9.1.0)
- FBSDKLoginKit (~> 9.1.0)
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_secure_storage (3.3.1):
- Flutter
- flutter_uxcam (2.0.0-beta.1):
@ -215,6 +213,8 @@ PODS:
- Sentry (~> 7.0.3)
- shared_preferences (0.0.1):
- Flutter
- sign_in_with_apple (0.0.1):
- Flutter
- sqflite (0.0.2):
- Flutter
- FMDB (>= 2.7.5)
@ -229,7 +229,7 @@ PODS:
- Flutter
DEPENDENCIES:
- apple_sign_in (from `.symlinks/plugins/apple_sign_in/ios`)
- awesome_notifications (from `.symlinks/plugins/awesome_notifications/ios`)
- device_info (from `.symlinks/plugins/device_info/ios`)
- devicelocale (from `.symlinks/plugins/devicelocale/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
@ -237,11 +237,10 @@ DEPENDENCIES:
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- firebase_remote_config (from `.symlinks/plugins/firebase_remote_config/ios`)
- flurry (from `.symlinks/plugins/flurry/ios`)
- flurry_data (from `.symlinks/plugins/flurry_data/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`)
@ -252,6 +251,7 @@ DEPENDENCIES:
- purchases_flutter (from `.symlinks/plugins/purchases_flutter/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
- video_player (from `.symlinks/plugins/video_player/ios`)
@ -289,8 +289,8 @@ SPEC REPOS:
- UXCam
EXTERNAL SOURCES:
apple_sign_in:
:path: ".symlinks/plugins/apple_sign_in/ios"
awesome_notifications:
:path: ".symlinks/plugins/awesome_notifications/ios"
device_info:
:path: ".symlinks/plugins/device_info/ios"
devicelocale:
@ -305,16 +305,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_messaging/ios"
firebase_remote_config:
:path: ".symlinks/plugins/firebase_remote_config/ios"
flurry:
:path: ".symlinks/plugins/flurry/ios"
flurry_data:
:path: ".symlinks/plugins/flurry_data/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:
@ -335,6 +333,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sentry_flutter/ios"
shared_preferences:
:path: ".symlinks/plugins/shared_preferences/ios"
sign_in_with_apple:
:path: ".symlinks/plugins/sign_in_with_apple/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
url_launcher:
@ -348,7 +348,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7
apple_sign_in: 7716c7ddfa195aeab7dec0dc374ef4ff45d1adb4
awesome_notifications: 74462bc8e68b11f8235d78422266886759e9da61
device_info: d7d233b645a32c40dfdc212de5cf646ca482f175
devicelocale: b22617f40038496deffba44747101255cee005b0
FBSDKCoreKit: a00fe2efd780c195a5e09201bf51c56106245b40
@ -367,12 +367,11 @@ SPEC CHECKSUMS:
FirebaseInstallations: c4aab1005d6547b00a7529777fe52f5d4d45165b
FirebaseMessaging: 1a33b4af3c8042ed6ddacb6c031894af2064bfab
FirebaseRemoteConfig: 055f6b5ba1751547596ded5032c4d5c6054ca501
flurry: 15b01f664ab1367c62b50291541ea7f78ca85aad
Flurry-iOS-SDK: 5831da8fc6bedb31fa1f94aac6fd204d36dd351d
flurry_data: 49b7066a283aa41f4306974c1f2d74c61231ad74
Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
flutter_app_badger: 65de4d6f0c34a891df49e6cfb8a1c0496426fa68
flutter_facebook_auth: 4b170c07b7fce791497093fcc3f134fb215f3f07
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
flutter_uxcam: ab8e5d3954eb448febd581375e2622e9eecb1066
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
@ -396,6 +395,7 @@ SPEC CHECKSUMS:
Sentry: 5b16f877da362d23716d827e04db642455b26b40
sentry_flutter: 602dc1902e152269256115e2386e1029511f3440
shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d
sign_in_with_apple: 34f3f5456a45fd7ac5fb42905e2ad31dae061b4a
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
UXCam: c2c00873595ab89be227f197213dc3679ff88ae5

View File

@ -388,7 +388,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = SFJJBDCU6Z;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -405,7 +405,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.1.18;
MARKETING_VERSION = 1.1.20;
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 = 11;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = SFJJBDCU6Z;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -548,7 +548,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.1.18;
MARKETING_VERSION = 1.1.20;
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 = 11;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = SFJJBDCU6Z;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -583,7 +583,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.1.18;
MARKETING_VERSION = 1.1.20;
PRODUCT_BUNDLE_IDENTIFIER = com.aitrainer.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

View File

@ -87,9 +87,9 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> with Trans {
yield LoginSuccess();
} else if (event is RegistrationSubmit) {
yield LoginLoading();
if (!this.dataPolicyAllowed) {
/* if (!this.dataPolicyAllowed) {
throw Exception("Please accept our data policy");
}
} */
final String? validationError = validate();
if (validationError != null) {
yield LoginError(message: validationError);
@ -104,9 +104,9 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> with Trans {
}
} else if (event is RegistrationFB) {
yield LoginLoading();
if (!this.dataPolicyAllowed) {
/* if (!this.dataPolicyAllowed) {
throw Exception("Please accept our data policy");
}
} */
Cache().setLoginType(LoginType.fb);
await userRepository.addUserFB();
accountBloc.add(AccountLogInFinished(customer: Cache().userLoggedIn!));
@ -116,9 +116,9 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> with Trans {
yield LoginSuccess();
} else if (event is RegistrationGoogle) {
yield LoginLoading();
if (!this.dataPolicyAllowed) {
/* if (!this.dataPolicyAllowed) {
throw Exception("Please accept our data policy");
}
} */
Cache().setLoginType(LoginType.google);
await userRepository.addUserGoogle();
accountBloc.add(AccountLogInFinished(customer: Cache().userLoggedIn!));
@ -128,9 +128,9 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> with Trans {
yield LoginSuccess();
} else if (event is RegistrationApple) {
yield LoginLoading();
if (!this.dataPolicyAllowed) {
/* if (!this.dataPolicyAllowed) {
throw Exception("Please accept our data policy");
}
} */
Cache().setLoginType(LoginType.apple);
await userRepository.addUserApple();
accountBloc.add(AccountLogInFinished(customer: Cache().userLoggedIn!));

View File

@ -58,15 +58,16 @@ class TrainingPlanBloc extends Bloc<TrainingPlanEvent, TrainingPlanState> {
await Cache().saveMyTrainingPlan();
yield TrainingPlanFinished();
} else if (event is TrainingPlanWeightChange) {
yield TrainingPlanLoading();
yield TrainingPlanExerciseLoading();
event.detail.weight = event.weight;
yield TrainingPlanExerciseReady();
yield TrainingPlanReady();
} else if (event is TrainingPlanRepeatsChange) {
yield TrainingPlanLoading();
yield TrainingPlanExerciseLoading();
event.detail.repeats = event.repeats;
yield TrainingPlanExerciseReady();
yield TrainingPlanReady();
} else if (event is TrainingPlanSetChange) {
yield TrainingPlanLoading();
@ -91,10 +92,13 @@ class TrainingPlanBloc extends Bloc<TrainingPlanEvent, TrainingPlanState> {
} else if (event.detail.exercises.length >= 0) {
event.detail.state = ExercisePlanDetailState.inProgress;
}
// recalculate the weight to the original planned repeats
if (event.detail.isTest && event.detail.exercises.length == 1) {
trainingPlanRepository.recalculateDetail(_myPlan!.trainingPlanId!, event.detail);
// recalculate the weight to the original planned repeats for the next details
if (exercise.unitQuantity != null && exercise.unitQuantity! > 0) {
for (var nextDetail in _myPlan!.details) {
if (nextDetail.exerciseTypeId == event.detail.exerciseTypeId && nextDetail.weight == -2) {
trainingPlanRepository.recalculateDetail(_myPlan!.trainingPlanId!, event.detail, nextDetail);
}
}
}
exercise.trainingPlanDetailsId = _myPlan!.trainingPlanId;
@ -230,28 +234,61 @@ class TrainingPlanBloc extends Bloc<TrainingPlanEvent, TrainingPlanState> {
dayNames.clear();
_myPlan!.days.clear();
String dayName = ".";
String previousDay = ".";
_myPlan!.details.forEach((element) {
if (element.day != null && element.day != dayName) {
dayNames.add(element.day!);
dayName = element.day!;
if (previousDay != ".") {
this.addExtraExerciseType("Stretching", previousDay);
}
}
if (_myPlan!.days[dayName] == null) {
if (dayName == ".") {
dayName = "";
}
_myPlan!.days[dayName] = [];
this.addExtraExerciseType("Warming Up", dayName);
previousDay = dayName;
}
_myPlan!.days[dayName]!.add(element);
});
if (dayNames.length == 0) {
dayNames.add("");
_myPlan!.days[""] = [];
_myPlan!.days[""]!.addAll(_myPlan!.details);
dayName = "";
dayNames.add(dayName);
_myPlan!.days[dayName] = [];
_myPlan!.days[dayName]!.addAll(_myPlan!.details);
}
getActiveDayIndex();
}
void addExtraExerciseType(String name, String dayName) {
if (Cache().getExerciseTypes() == null) {
return;
}
for (var exerciseType in Cache().getExerciseTypes()!) {
if (exerciseType.name == name) {
CustomerTrainingPlanDetails detail = CustomerTrainingPlanDetails();
detail.customerTrainingPlanDetailsId = 0;
detail.trainingPlanDetailsId = 0;
detail.exerciseTypeId = exerciseType.exerciseTypeId;
detail.repeats = 1;
detail.set = 1;
detail.day = "";
detail.parallel = false;
detail.restingTime = 0;
detail.exerciseType = exerciseType;
if (_myPlan!.days[dayName] == null) {
_myPlan!.days[dayName] = [];
}
_myPlan!.days[dayName]!.add(detail);
break;
}
}
}
CustomerTrainingPlanDetails? getTrainingPlanDetail(int trainingPlanDetailsId) {
CustomerTrainingPlanDetails? detail;
if (_myPlan == null || _myPlan!.details.isEmpty) {
@ -297,13 +334,14 @@ class TrainingPlanBloc extends Bloc<TrainingPlanEvent, TrainingPlanState> {
int minStep = 99;
for (final detail in this._myPlan!.details) {
if (!detail.state.equalsTo(ExercisePlanDetailState.finished)) {
if (detail.exercises.isEmpty && !detail.state.equalsTo(ExercisePlanDetailState.skipped)) {
final day = dayNames[this.activeDayIndex];
if (detail.exercises.isEmpty && !detail.state.equalsTo(ExercisePlanDetailState.skipped) && day == detail.day) {
next = detail;
minStep = 1;
break;
} else {
final int step = detail.exercises.length;
if (step < minStep && !detail.state.equalsTo(ExercisePlanDetailState.skipped)) {
if (step < minStep && !detail.state.equalsTo(ExercisePlanDetailState.skipped) && day == detail.day) {
next = detail;
minStep = step;
if (detail.parallel != true) {
@ -313,7 +351,7 @@ class TrainingPlanBloc extends Bloc<TrainingPlanEvent, TrainingPlanState> {
}
}
}
print("Next detail $next");
//print("Next detail $next");
return next;
}
@ -379,20 +417,19 @@ class TrainingPlanBloc extends Bloc<TrainingPlanEvent, TrainingPlanState> {
}
int getActiveDayIndex() {
activeDayIndex = 0;
if (restarting) {
return 0;
}
if (_myPlan == null || _myPlan!.details.isEmpty) {
// throw Exception("No defined Training Plan");
return 0;
}
if (dayNames.isEmpty || dayNames.length == 1) {
activeDayIndex = 0;
return 0;
}
activeDayIndex = 0;
for (var day in dayNames) {
if (_myPlan!.days[day] == null) {
throw Exception("Wrong activated day: $day does not exist");
@ -413,7 +450,7 @@ class TrainingPlanBloc extends Bloc<TrainingPlanEvent, TrainingPlanState> {
activeDayIndex = 0;
this.add(TrainingPlanGoToRestart());
}
print("ActiveDayIndex $activeDayIndex");
return activeDayIndex;
}

View File

@ -15,10 +15,18 @@ class TrainingPlanLoading extends TrainingPlanState {
const TrainingPlanLoading();
}
class TrainingPlanExerciseLoading extends TrainingPlanState {
const TrainingPlanExerciseLoading();
}
class TrainingPlanReady extends TrainingPlanState {
const TrainingPlanReady();
}
class TrainingPlanExerciseReady extends TrainingPlanState {
const TrainingPlanExerciseReady();
}
class TrainingPlanFinished extends TrainingPlanState {
const TrainingPlanFinished();
}

View File

@ -3,7 +3,6 @@ import 'dart:io';
import 'package:aitrainer_app/bloc/test_set_execute/test_set_execute_bloc.dart';
import 'package:aitrainer_app/bloc/training_plan/training_plan_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/training_plan_repository.dart';
import 'package:aitrainer_app/repository/workout_tree_repository.dart';
@ -44,7 +43,7 @@ import 'package:aitrainer_app/widgets/home.dart';
import 'package:aitrainer_app/library/facebook_app_events/facebook_app_events.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observer.dart';
import 'package:flurry/flurry.dart';
import 'package:flurry_data/flurry_data.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
@ -110,6 +109,7 @@ Future<Null> main() async {
if (isInDebugMode) {
// In development mode simply print to console.
FlutterError.dumpErrorToConsole(details);
FlurryData.logEvent("enter_test");
} else {
// In production mode report to the application zone to report to
// Sentry.
@ -192,10 +192,9 @@ Future<Null> main() async {
Future<void> initThirdParty() async {
if (!isInDebugMode) {
await Flurry.initialize(androidKey: "JNYCTCWBT34FM3J8TV36", iosKey: "3QBG7BSMGPDH24S8TRQP", enableLog: true);
await FlurryData.initialize(androidKey: "JNYCTCWBT34FM3J8TV36", iosKey: "3QBG7BSMGPDH24S8TRQP", enableLog: true);
FlutterUxcam.optIntoSchematicRecordings();
}
PushNotificationsManager().init();
}
class WorkoutTestApp extends StatelessWidget {

View File

@ -19,6 +19,7 @@ import 'package:aitrainer_app/model/purchase.dart';
import 'package:aitrainer_app/model/split_test.dart';
import 'package:aitrainer_app/model/sport.dart';
import 'package:aitrainer_app/model/training_plan.dart';
import 'package:aitrainer_app/model/training_plan_day.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';
@ -30,7 +31,7 @@ import 'package:aitrainer_app/util/enums.dart';
import 'package:aitrainer_app/util/env.dart';
import 'package:aitrainer_app/util/track.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flurry/flurry.dart';
import 'package:flurry_data/flurry_data.dart';
import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
import 'package:flutter_uxcam/flutter_uxcam.dart';
import 'package:package_info/package_info.dart';
@ -137,6 +138,7 @@ class Cache with Logging {
List<wt_product.Product>? _products;
List<Purchase> _purchases = [];
List<SplitTest> _splitTests = [];
List<TrainingPlanDay> _trainingPlanDays = [];
List<ExercisePlanTemplate> _exercisePlanTemplates = [];
@ -184,6 +186,7 @@ class Cache with Logging {
String testEnv = EnvironmentConfig.test_env;
this.testEnvironment = testEnv;
if (testEnv == "1") {
print("testEnv $testEnv");
baseUrl = baseUrlTest;
liveServer = false;
}
@ -668,7 +671,7 @@ class Cache with Logging {
await PackageApi().getCustomerPackage(customerId);
if (!isInDebugMode) {
Flurry.setUserId(customerId.toString());
FlurryData.setUserId(customerId.toString());
//Smartlook.setUserIdentifier(customerId.toString());
FlutterUxcam.setUserProperty("username", customerId.toString());
FlutterUxcam.setUserIdentity(customerId.toString());
@ -737,4 +740,7 @@ class Cache with Logging {
List<SplitTest> getSplitTests() => this._splitTests;
setSplitTests(value) => this._splitTests = value;
List<TrainingPlanDay> getTrainingPlanDays() => this._trainingPlanDays;
setTrainingPlanDays(value) => this._trainingPlanDays = value;
}

View File

@ -4,7 +4,7 @@ class CustomerProperty {
int? customerPropertyId;
late int propertyId;
late int customerId;
late DateTime dateAdd;
DateTime? dateAdd;
late double propertyValue;
bool newData = false;
@ -14,7 +14,7 @@ class CustomerProperty {
this.customerPropertyId = json['customerPropertyId'];
this.propertyId = json['propertyId'];
this.customerId = json['customerId'];
this.dateAdd = json['propertyName'];
this.dateAdd = json['dataAdd'] ?? DateTime.now();
this.propertyValue = json['propertyValue'];
}
@ -24,14 +24,14 @@ class CustomerProperty {
"customerPropertyId": this.customerPropertyId,
"propertyId": this.propertyId,
"customerId": this.customerId,
"dateAdd": DateFormat('yyyy-MM-dd HH:mm:ss').format(this.dateAdd),
"dateAdd": DateFormat('yyyy-MM-dd HH:mm:ss').format(this.dateAdd!),
"propertyValue": this.propertyValue
};
} else {
return {
"propertyId": this.propertyId,
"customerId": this.customerId,
"dateAdd": DateFormat('yyyy-MM-dd HH:mm:ss').format(this.dateAdd),
"dateAdd": DateFormat('yyyy-MM-dd HH:mm:ss').format(this.dateAdd!),
"propertyValue": this.propertyValue
};
}

View File

@ -2,6 +2,7 @@ import 'package:aitrainer_app/model/cache.dart';
import 'package:aitrainer_app/model/exercise.dart';
import 'package:aitrainer_app/model/exercise_plan_detail.dart';
import 'package:aitrainer_app/model/exercise_type.dart';
import 'package:aitrainer_app/repository/training_plan_day_repository.dart';
class CustomerTrainingPlanDetails {
/// customerTrainingPlanDetails
@ -26,6 +27,8 @@ class CustomerTrainingPlanDetails {
bool? parallel;
String? day;
int? dayId;
/// exerciseType
ExerciseType? exerciseType;
@ -63,10 +66,10 @@ class CustomerTrainingPlanDetails {
: json['parallel'] == "true"
? true
: null;
this.day = json['day'].toString();
if (this.day == null || this.day == "null") {
this.day = "";
}
this.dayId = json['dayId'] == "null" ? null : json['dayId'];
TrainingPlanDayRepository trainingPlanDayRepository = TrainingPlanDayRepository();
this.day = trainingPlanDayRepository.getNameById(this.dayId);
try {
Iterable iterable = json['exercises'];
this.exercises = iterable.map((exercise) => Exercise.fromJson(exercise)).toList();
@ -114,15 +117,30 @@ class CustomerTrainingPlanDetails {
'exercises': exercises.isEmpty ? [].toString() : exercises.map((exercise) => exercise.toJson()).toList().toString(),
'state': this.state.toStr(),
"isTest": this.isTest,
"dayId": this.dayId,
};
if (this.day != null && this.day!.isNotEmpty) {
jsonMap["day"] = this.day;
}
//print("Detail $jsonMap");
//print("Detail toJson $jsonMap");
return jsonMap;
}
@override
String toString() => this.toJsonWithExercises().toString();
void copy(CustomerTrainingPlanDetails from) {
this.customerTrainingPlanDetailsId = from.customerTrainingPlanDetailsId;
this.trainingPlanDetailsId = from.trainingPlanDetailsId;
this.exerciseTypeId = from.exerciseTypeId;
this.exerciseType = from.exerciseType;
this.set = from.set;
this.repeats = from.repeats;
this.weight = from.weight;
this.restingTime = from.restingTime;
this.parallel = from.parallel;
this.exercises = from.exercises;
this.state = from.state;
this.isTest = from.isTest;
this.day = from.day;
this.dayId = from.dayId;
}
}

View File

@ -1,10 +1,10 @@
import 'package:aitrainer_app/model/evaluation_attribute.dart';
class Evaluation {
late int evaluationId;
int? evaluationId;
late String name;
late int exerciseTypeId;
late String unit;
int? exerciseTypeId;
String? unit;
late List attributes;
Evaluation.fromJson(Map json) {

View File

@ -1,6 +1,6 @@
class EvaluationAttribute {
late int evaluationAttrId;
late int evaluationId;
int? evaluationId;
late String name;
late String sex;
late int ageMin;

View File

@ -17,7 +17,7 @@ class ExerciseType {
late String unit;
/// unitQuantity
late String unitQuantity;
String? unitQuantity;
/// unitQuantityUnit
String? unitQuantityUnit;
@ -28,6 +28,8 @@ class ExerciseType {
/// base
late bool base;
late bool buddyWarning;
/// imageUrl
String imageUrl = "";
@ -65,6 +67,7 @@ class ExerciseType {
this.unitQuantityUnit = json['unitQuantityUnit'];
this.active = json['active'];
this.base = json['base'];
this.buddyWarning = json['buddyWarning'];
if (json['images'].length > 0) {
this.imageUrl = json['images'][0]['url'];
}
@ -105,6 +108,8 @@ class ExerciseType {
"unitQuantity": unitQuantity,
"unitQuantityUnit": unitQuantityUnit,
"active": active,
"base": base,
"buddyWarning": buddyWarning,
"devices": this.devices.toString(),
"nameTranslation": this.nameTranslation,
"parents": this.parents.toString()

View File

@ -4,10 +4,10 @@ import 'package:aitrainer_app/model/training_plan_detail.dart';
class TrainingPlan {
late int trainingPlanId;
late String type;
String? type;
late String name;
late String internalName;
late String description;
String? internalName;
String? description;
late bool free;
late bool active;
int? treeId;
@ -28,7 +28,7 @@ class TrainingPlan {
this.treeId = json['treeId'];
nameTranslations['en'] = name;
descriptionTranslations['en'] = description;
descriptionTranslations['en'] = description ?? "";
if (json['translations'] != null && json['translations'].length > 0) {
json['translations'].forEach((translation) {
nameTranslations[translation['languageCode']] = translation['nameTranslation'];

View File

@ -0,0 +1,29 @@
import 'dart:collection';
class TrainingPlanDay {
late int dayId;
late String name;
HashMap<String, String> nameTranslations = HashMap();
TrainingPlanDay.fromJson(Map json) {
this.dayId = json['dayId'];
this.name = json['name'];
nameTranslations['en'] = name;
if (json['translations'] != null && json['translations'].length > 0) {
json['translations'].forEach((translation) {
nameTranslations[translation['languageCode']] = translation['nameTranslation'];
});
}
}
Map<String, dynamic> toJson() => {
"dayId": this.dayId,
"name": this.name,
"nameTranslation": this.nameTranslations.toString(),
};
@override
String toString() => this.toJson().toString();
}

View File

@ -1,14 +1,15 @@
class TrainingPlanDetail {
late int trainingPlanDetailId;
late int trainingPlanId;
int? trainingPlanId;
late int exerciseTypeId;
late int sort;
late int set;
late int repeats;
late double weight;
late int restingTime;
late bool parallel;
late String day;
int? repeats;
double? weight;
int? restingTime;
bool? parallel;
int? dayId;
String? day;
TrainingPlanDetail.fromJson(Map<String, dynamic> json) {
this.trainingPlanDetailId = json['trainingPlanDetailId'];
@ -20,7 +21,7 @@ class TrainingPlanDetail {
this.weight = json['weight'];
this.restingTime = json['restingTime'];
this.parallel = json['parallel'];
this.day = json['day'];
this.dayId = json['dayId'];
}
Map<String, dynamic> toJson() => {
@ -32,6 +33,7 @@ class TrainingPlanDetail {
"weight": this.weight,
"restingTime": this.restingTime,
"parallel": this.parallel,
"dayId": this.dayId,
"day": this.day,
};

View File

@ -1,34 +0,0 @@
import 'package:aitrainer_app/service/logging.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class PushNotificationsManager with Logging {
PushNotificationsManager._();
factory PushNotificationsManager() => _instance;
static final PushNotificationsManager _instance = PushNotificationsManager._();
Future<void> init() async {
log(" --- Firebase Messagein init..");
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'high_importance_channel', // id
'High Importance Notifications', // title
'This channel is used for important notifications.', // description
importance: Importance.max,
);
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
alert: true, // Required to display a heads up notification
badge: true,
sound: true,
);
String? token = await FirebaseMessaging.instance.getToken();
log("FirebaseMessaging token $token");
}
}

View File

@ -0,0 +1,35 @@
import 'package:aitrainer_app/model/cache.dart';
import 'package:aitrainer_app/model/training_plan.dart';
import 'package:aitrainer_app/util/app_language.dart';
class TrainingPlanDayRepository {
const TrainingPlanDayRepository();
void assignTrainingPlanDays() {
List<TrainingPlan>? plans = Cache().getTrainingPlans();
if (plans == null) {
return;
}
plans.forEach((plan) {
if (plan.details != null) {
plan.details!.forEach((element) {
element.day = this.getNameById(element.dayId);
});
}
});
}
String? getNameById(int? dayId) {
if (dayId == null) {
return "";
}
String? name;
for (var day in Cache().getTrainingPlanDays()) {
if (day.dayId == dayId) {
name = day.nameTranslations[AppLanguage().appLocal.languageCode];
break;
}
}
return name;
}
}

View File

@ -5,6 +5,7 @@ import 'package:aitrainer_app/model/exercise.dart';
import 'package:aitrainer_app/model/exercise_plan_detail.dart';
import 'package:aitrainer_app/model/exercise_tree.dart';
import 'package:aitrainer_app/model/training_plan.dart';
import 'package:aitrainer_app/repository/training_plan_day_repository.dart';
import 'package:aitrainer_app/service/training_plan_service.dart';
import 'package:aitrainer_app/util/common.dart';
@ -66,6 +67,7 @@ class TrainingPlanRepository {
// 3 calculate weights
int index = 0;
trainingPlan.details!.forEach((elem) {
CustomerTrainingPlanDetails detail = CustomerTrainingPlanDetails();
detail.customerTrainingPlanDetailsId = ++index;
@ -73,7 +75,9 @@ class TrainingPlanRepository {
detail.exerciseTypeId = elem.exerciseTypeId;
detail.repeats = elem.repeats;
detail.set = elem.set;
detail.day = elem.day;
detail.dayId = elem.dayId;
TrainingPlanDayRepository trainingPlanDayRepository = TrainingPlanDayRepository();
detail.day = trainingPlanDayRepository.getNameById(elem.dayId);
detail.parallel = elem.parallel;
detail.restingTime = elem.restingTime;
detail.exerciseType = Cache().getExerciseTypeById(detail.exerciseTypeId!);
@ -83,6 +87,13 @@ class TrainingPlanRepository {
} else {
detail.weight = 0;
}
} else if (elem.weight == -2) {
final CustomerTrainingPlanDetails calculated = this.isWeightCalculatedByExerciseType(elem.exerciseTypeId, detail, plan);
if (calculated.weight != -1) {
detail.weight = calculated.weight;
} else {
detail.weight = -2;
}
} else {
detail.weight = elem.weight;
}
@ -98,6 +109,19 @@ class TrainingPlanRepository {
return plan;
}
CustomerTrainingPlanDetails isWeightCalculatedByExerciseType(
int exerciseTypeId, CustomerTrainingPlanDetails detail, CustomerTrainingPlan plan) {
CustomerTrainingPlanDetails calculated = detail;
for (var element in plan.details) {
if (element.exerciseTypeId == exerciseTypeId) {
calculated = element;
break;
}
}
return calculated;
}
TrainingPlan? getTrainingPlanById(int trainingPlanId) {
TrainingPlan? plan;
if (Cache().getTrainingPlans() == null) {
@ -148,8 +172,9 @@ class TrainingPlanRepository {
return detail;
}
CustomerTrainingPlanDetails recalculateDetail(int trainingPlanId, CustomerTrainingPlanDetails detail) {
CustomerTrainingPlanDetails recalculatedDetail = detail;
CustomerTrainingPlanDetails recalculateDetail(
int trainingPlanId, CustomerTrainingPlanDetails detail, CustomerTrainingPlanDetails nextDetail) {
CustomerTrainingPlanDetails recalculatedDetail = nextDetail;
// 1. get original repeats
@ -164,14 +189,14 @@ class TrainingPlanRepository {
plan.details!.forEach((element) {
if (element.trainingPlanDetailId == detail.trainingPlanDetailsId) {
print("element $element");
originalRepeats = element.repeats;
originalRepeats = element.repeats ?? 0;
}
});
// 2 get recalculated repeats
recalculatedDetail.weight =
Common.calculateWeigthByChangedQuantity(detail.weight!, detail.repeats!.toDouble(), originalRepeats.toDouble());
recalculatedDetail.weight = Common.roundWeight(recalculatedDetail.weight!);
print("recalculated repeats for $originalRepeats: ${recalculatedDetail.weight}");
recalculatedDetail.repeats = originalRepeats;

View File

@ -112,10 +112,10 @@ class WorkoutTreeRepository with Logging {
0,
"");
this.tree[exerciseType.name] = menuItem;
if (isRunning || is1RM) {
if (isRunning || is1RM || exerciseType.name == "Warming Up" || exerciseType.name == "Stretching") {
menuAsExercise.add(menuItem);
}
//log("ExerciseType in Menu item ${exerciseType.toJson()} is1RM: $is1RM");
//log("ExerciseType in Menu item ${exerciseType.toJson()}");
});
} else {
//log("No Parents " + exerciseType.toJson().toString());

View File

@ -1,15 +1,20 @@
import 'dart:math' as math;
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:aitrainer_app/model/cache.dart';
import 'package:aitrainer_app/service/logging.dart' as logging;
import 'package:apple_sign_in/apple_sign_in.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:awesome_notifications/awesome_notifications.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/material.dart';
import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
class FirebaseApi with logging.Logging {
bool appleSignInAvailable = false;
//late FirebaseApi _instance;
static final FirebaseAuth auth = FirebaseAuth.instance;
@ -29,15 +34,69 @@ class FirebaseApi with logging.Logging {
Future<void> initializeFlutterFire() async {
try {
// Wait for Firebase to initialize and set `_initialized` state to true
FirebaseApp app = await Firebase.initializeApp();
await Firebase.initializeApp();
this.appleSignInAvailable = await AppleSignIn.isAvailable();
this.appleSignInAvailable = await SignInWithApple.isAvailable();
AwesomeNotifications().initialize(
// set the icon to null if you want to use the default app icon
null,
[
NotificationChannel(
channelKey: 'basic_channel',
channelName: 'Basic notifications',
channelDescription: 'Notification channel for basic tests',
defaultColor: Color(0xFF9D50DD),
ledColor: Colors.white)
]);
AwesomeNotifications().isNotificationAllowed().then((isAllowed) {
if (!isAllowed) {
// Insert here your friendly dialog box before call the request method
// This is very important to not harm the user experience
AwesomeNotifications().requestPermissionToSendNotifications();
}
});
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
alert: true, // Required to display a heads up notification
badge: true,
sound: true,
);
String? token = await FirebaseMessaging.instance.getToken();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
log("FirebaseMessaging token $token");
} catch (e) {
// Set `_error` state to true if Firebase initialization fails
log("Error initializing Firebase");
}
}
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
print('Handling a background message: ${message.messageId}');
if (!StringUtils.isNullOrEmpty(message.notification?.title, considerWhiteSpaceAsEmpty: true) ||
!StringUtils.isNullOrEmpty(message.notification?.body, considerWhiteSpaceAsEmpty: true)) {
print('message also contained a notification: ${message.notification}');
String? imageUrl;
imageUrl ??= message.notification!.android?.imageUrl;
imageUrl ??= message.notification!.apple?.imageUrl;
Map<String, dynamic> notificationAdapter = {
NOTIFICATION_CHANNEL_KEY: 'basic_channel',
NOTIFICATION_ID: message.data[NOTIFICATION_CONTENT]?[NOTIFICATION_ID] ?? message.messageId ?? math.Random().nextInt(2147483647),
NOTIFICATION_TITLE: message.data[NOTIFICATION_CONTENT]?[NOTIFICATION_TITLE] ?? message.notification?.title,
NOTIFICATION_BODY: message.data[NOTIFICATION_CONTENT]?[NOTIFICATION_BODY] ?? message.notification?.body,
NOTIFICATION_LAYOUT: StringUtils.isNullOrEmpty(imageUrl) ? 'Default' : 'BigPicture',
NOTIFICATION_BIG_PICTURE: imageUrl
};
AwesomeNotifications().createNotificationFromJsonData(notificationAdapter);
} else {
AwesomeNotifications().createNotificationFromJsonData(message.data);
}
}
Future<String> signInEmail(String? email, String? password) async {
if (email == null) {
throw Exception("Please type an email address");
@ -85,20 +144,33 @@ class FirebaseApi with logging.Logging {
return rc;
}
String generateNonce([int length = 32]) {
final charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
final random = math.Random.secure();
return List.generate(length, (_) => charset[random.nextInt(charset.length)]).join();
}
/// Returns the sha256 hash of [input] in hex notation.
String sha256ofString(String input) {
final bytes = utf8.encode(input);
final digest = sha256.convert(bytes);
return digest.toString();
}
Future<Map<String, dynamic>> signInWithApple() async {
Map<String, dynamic> userData = Map();
final AuthorizationResult result = await AppleSignIn.performRequests([
AppleIdRequest(requestedScopes: [Scope.email, Scope.fullName])
/* final apple.AuthorizationResult result = await SignInWithApple.performRequests([
apple.AppleIdRequest(requestedScopes: [apple.Scope.email, apple.Scope.fullName])
]);
switch (result.status) {
case AuthorizationStatus.authorized:
case apple.AuthorizationStatus.authorized:
print('User authorized');
break;
case AuthorizationStatus.error:
case apple.AuthorizationStatus.error:
print('User error');
throw Exception("Apple Sign-In failed");
case AuthorizationStatus.cancelled:
case apple.AuthorizationStatus.cancelled:
print('User cancelled');
throw Exception("Apple Sign-In cancelled");
}
@ -106,20 +178,36 @@ class FirebaseApi with logging.Logging {
// Create an `OAuthCredential` from the credential returned by Apple.
final oauthCredential = OAuthProvider("apple.com").credential(
idToken: String.fromCharCodes(result.credential.identityToken),
accessToken: String.fromCharCodes(result.credential.authorizationCode));
accessToken: String.fromCharCodes(result.credential.authorizationCode!));
*/
// To prevent replay attacks with the credential returned from Apple, we
// include a nonce in the credential request. When signing in with
// Firebase, the nonce in the id token returned by Apple, is expected to
// match the sha256 hash of `rawNonce`.
final rawNonce = generateNonce();
final nonce = sha256ofString(rawNonce);
// Request credential for the currently signed in Apple account.
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
nonce: nonce,
);
// Create an `OAuthCredential` from the credential returned by Apple.
final oauthCredential = OAuthProvider("apple.com").credential(
idToken: appleCredential.identityToken,
rawNonce: rawNonce,
);
// Sign in the user with Firebase. If the nonce we generated earlier does
// not match the nonce in `appleCredential.identityToken`, sign in will fail.
UserCredential userCredential = await FirebaseAuth.instance.signInWithCredential(oauthCredential);
Cache().firebaseUid = userCredential.user!.uid;
log("userCredential: " + userCredential.toString());
log("Apple Credentials: " +
result.credential.user.toString() +
" state " +
result.credential.state.toString() +
" email " +
userCredential.user!.email!);
log("Apple Credentials: ${appleCredential.userIdentifier} state ${appleCredential.state} email ${userCredential.user!.email!}");
userData['email'] = userCredential.user!.email;
return userData;
@ -127,26 +215,43 @@ class FirebaseApi with logging.Logging {
Future<Map<String, dynamic>> registerWithApple() async {
Map<String, dynamic> userData = Map();
final AuthorizationResult result = await AppleSignIn.performRequests([
AppleIdRequest(requestedScopes: [Scope.email, Scope.fullName])
/* final apple.AuthorizationResult result = await apple.TheAppleSignIn.performRequests([
apple.AppleIdRequest(requestedScopes: [apple.Scope.email, apple.Scope.fullName])
]);
switch (result.status) {
case AuthorizationStatus.authorized:
case apple.AuthorizationStatus.authorized:
print('Apple User authorized');
break;
case AuthorizationStatus.error:
case apple.AuthorizationStatus.error:
print('Apple User error');
throw Exception("Apple Sign-In failed");
case AuthorizationStatus.cancelled:
case apple.AuthorizationStatus.cancelled:
print('User cancelled');
throw Exception("Apple Sign-In cancelled");
}
*/
final rawNonce = generateNonce();
final nonce = sha256ofString(rawNonce);
// Create an `OAuthCredential` from the credential returned by Apple.
// Request credential for the currently signed in Apple account.
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
nonce: nonce,
);
final oauthCredential = OAuthProvider("apple.com").credential(
idToken: appleCredential.identityToken,
rawNonce: rawNonce,
);
/* // Create an `OAuthCredential` from the credential returned by Apple.
final oauthCredential = OAuthProvider("apple.com").credential(
idToken: String.fromCharCodes(result.credential.identityToken),
accessToken: String.fromCharCodes(result.credential.authorizationCode));
*/
// Sign in the user with Firebase. If the nonce we generated earlier does
// not match the nonce in `appleCredential.identityToken`, sign in will fail.
UserCredential userCredential = await FirebaseAuth.instance.signInWithCredential(oauthCredential);

View File

@ -19,7 +19,9 @@ import 'package:aitrainer_app/model/property.dart';
import 'package:aitrainer_app/model/purchase.dart';
import 'package:aitrainer_app/model/split_test.dart';
import 'package:aitrainer_app/model/training_plan.dart';
import 'package:aitrainer_app/model/training_plan_day.dart';
import 'package:aitrainer_app/model/tutorial.dart';
import 'package:aitrainer_app/repository/training_plan_day_repository.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';
@ -105,6 +107,10 @@ class PackageApi {
final List<SplitTest>? tests = json.map((test) => SplitTest.fromJson(test)).toList();
//print("A/B tests: $tests");
Cache().setSplitTests(tests);
} else if (headRecord[0] == "TrainingPlanDay") {
final Iterable json = jsonDecode(headRecord[1]);
final List<TrainingPlanDay>? days = json.map((day) => TrainingPlanDay.fromJson(day)).toList();
Cache().setTrainingPlanDays(days);
}
});
@ -114,9 +120,11 @@ class PackageApi {
ExerciseTree tree = element as ExerciseTree;
tree.imageUrl = await ExerciseTreeApi().buildImage(tree.imageUrl, tree.treeId);
});
//print("tree: $exerciseTree");
Cache().setExerciseTree(exerciseTree);
TrainingPlanDayRepository trainingPlanDayRepository = TrainingPlanDayRepository();
trainingPlanDayRepository.assignTrainingPlanDays();
return;
}

View File

@ -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:firebase_analytics/firebase_analytics.dart';
import 'package:flurry/flurry.dart';
import 'package:flurry_data/flurry_data.dart';
import 'package:flutter_uxcam/flutter_uxcam.dart';
class Track with Logging {
@ -20,7 +20,7 @@ class Track with Logging {
void track(TrackingEvent event, {String eventValue = ""}) {
if (!isInDebugMode) {
Flurry.logEvent(event.toString());
FlurryData.logEvent(event.toString());
// Smartlook.setGlobalEventProperty(event.toString(), eventValue, false);
FlutterUxcam.logEventWithProperties(event.enumToString(), {"value": eventValue});
model.Tracking tracking = model.Tracking();

View File

@ -202,8 +202,8 @@ class RegistrationPage extends StatelessWidget with Trans {
Divider(
color: Colors.transparent,
),
getDataProtection(loginBloc),
loginBloc.emailCheckbox ? getEmailSubscription(loginBloc) : Offstage(),
// getDataProtection(loginBloc),
// loginBloc.emailCheckbox ? getEmailSubscription(loginBloc) : Offstage(),
Divider(
color: Colors.transparent,
),

View File

@ -254,7 +254,7 @@ class TrainingPlanActivatePage extends StatelessWidget with Trans {
ElevatedButton(
style: ElevatedButton.styleFrom(
onPrimary: Colors.white,
primary: Colors.orange,
primary: restricted ? Colors.grey[600] : Colors.orange,
),
child: Text(t("Start")),
onPressed: () {
@ -294,15 +294,25 @@ class TrainingPlanActivatePage extends StatelessWidget with Trans {
}
},
),
restricted
/* restricted
? Container(
padding: EdgeInsets.only(bottom: 8),
child: Text(
t("This is a premium function"),
style: GoogleFonts.inter(color: Colors.blue[700]),
style: GoogleFonts.inter(
color: Colors.deepOrange[800],
fontWeight: FontWeight.bold,
shadows: <Shadow>[
Shadow(
offset: Offset(2.0, 2.0),
blurRadius: 4.0,
color: Colors.black54,
),
],
),
),
)
: Offstage(),
: Offstage(), */
]),
)));
@ -352,7 +362,7 @@ class TrainingPlanActivatePage extends StatelessWidget with Trans {
Widget getPlanDetails(TrainingPlan plan, TrainingPlanBloc bloc) {
return SfDataGrid(
headerRowHeight: 30,
rowHeight: 45,
rowHeight: 60,
source: TrainingPlanDetailSource(
plan: plan,
menuBloc: bloc.menuBloc,
@ -390,14 +400,33 @@ class TrainingPlanActivatePage extends StatelessWidget with Trans {
Navigator.of(context).pop(),
},
);
}),
},
onDropsetTap: () => {
showDialog(
context: context,
builder: (BuildContext context) {
return DialogCommon(
title: t("Dropset"),
descriptions: t("A drop set is an advanced resistance training technique "),
description2:
t(" in which you focus on completing a set until failure - or the inability to do another repetition."),
text: "OK",
onTap: () => {
Navigator.of(context).pop(),
},
onCancel: () => {
Navigator.of(context).pop(),
},
);
})
}),
headerGridLinesVisibility: GridLinesVisibility.both,
gridLinesVisibility: GridLinesVisibility.both,
columns: [
GridTextColumn(
//columnWidthMode: ColumnWidthMode.lastColumnFill,
maximumWidth: 120,
columnWidthMode: ColumnWidthMode.lastColumnFill,
maximumWidth: 160,
columnName: 'exerciseImage',
label: Container(
color: Colors.green[50],
@ -472,11 +501,13 @@ class TrainingPlanActivatePage extends StatelessWidget with Trans {
class TrainingPlanDetailSource extends DataGridSource {
final TrainingPlan plan;
final MenuBloc menuBloc;
final VoidCallback onDropsetTap;
final VoidCallback onWeightTap;
final VoidCallback onRepeatTap;
TrainingPlanDetailSource({
required this.plan,
required this.menuBloc,
required this.onDropsetTap,
required this.onWeightTap,
required this.onRepeatTap,
}) {
@ -540,7 +571,7 @@ class TrainingPlanDetailSource extends DataGridSource {
]),
))
])
: dataGridCell.columnName == "weight" && dataGridCell.value == -1
: dataGridCell.columnName == "weight" && (dataGridCell.value == -1 || dataGridCell.value == -2)
? GestureDetector(
onTap: () {
onWeightTap();
@ -549,6 +580,15 @@ class TrainingPlanDetailSource extends DataGridSource {
CustomIcon.question_circle,
color: Colors.indigo[300],
))
: dataGridCell.columnName == "weight" && dataGridCell.value == -3
? GestureDetector(
onTap: () {
onDropsetTap();
},
child: Icon(
CustomIcon.question_circle,
color: Colors.orange[400],
))
: dataGridCell.columnName == "reps" && dataGridCell.value == -1
? GestureDetector(
onTap: () {

View File

@ -8,8 +8,10 @@ import 'package:aitrainer_app/util/app_localization.dart';
import 'package:aitrainer_app/util/trans.dart';
import 'package:aitrainer_app/widgets/app_bar.dart';
import 'package:aitrainer_app/widgets/dialog_common.dart';
import 'package:aitrainer_app/widgets/dialog_html.dart';
import 'package:aitrainer_app/widgets/menu_image.dart';
import 'package:aitrainer_app/widgets/victory_widget.dart';
import 'package:badges/badges.dart';
import 'package:extended_tabs/extended_tabs.dart';
import 'package:ezanimation/ezanimation.dart';
import 'package:flutter/cupertino.dart';
@ -125,6 +127,7 @@ class _ExerciseTabs extends State<ExerciseTabs> with TickerProviderStateMixin {
@override
void initState() {
super.initState();
print("init TAB ${widget.bloc.dayNames.length} index ${widget.bloc.activeDayIndex}");
tabController = TabController(length: widget.bloc.dayNames.length, vsync: this);
tabController.animateTo(widget.bloc.activeDayIndex, duration: Duration(milliseconds: 300));
}
@ -143,6 +146,13 @@ class _ExerciseTabs extends State<ExerciseTabs> with TickerProviderStateMixin {
Widget getTabs(TrainingPlanBloc bloc) {
return Column(children: [
ExtendedTabBar(
indicator: BoxDecoration(
color: Colors.black87,
border: Border(
bottom: BorderSide(width: 4.0, color: Colors.blue),
top: BorderSide(width: 4.0, color: Colors.blue),
)),
labelPadding: EdgeInsets.only(left: 5, right: 5),
tabs: getTabNames(),
controller: tabController,
onTap: (index) => bloc.activeDayIndex = index,
@ -168,7 +178,12 @@ class _ExerciseTabs extends State<ExerciseTabs> with TickerProviderStateMixin {
List<Tab> getTabNames() {
List<Tab> tabs = [];
widget.bloc.dayNames.forEach((element) {
final Widget widget = RichText(
final Widget widget = Container(
//height: 50,
padding: EdgeInsets.only(top: 2, left: 5, right: 5, bottom: 2),
color: Colors.white24,
child: RichText(
textScaleFactor: 0.8,
text: TextSpan(
style: GoogleFonts.inter(
fontSize: 14,
@ -213,7 +228,7 @@ class _ExerciseTabs extends State<ExerciseTabs> with TickerProviderStateMixin {
),
],
)),
]));
])));
tabs.add(Tab(child: widget));
});
@ -559,11 +574,17 @@ class _ExerciseTileState extends State<ExerciseTile> with Trans {
@override
Widget build(BuildContext context) {
setContext(context);
print("detail ${widget.detail}");
final ExercisePlanDetailState state = widget.detail.state;
final bool done = state.equalsTo(ExercisePlanDetailState.finished) || state.equalsTo(ExercisePlanDetailState.skipped);
final String countSerie = widget.detail.set.toString();
final String step = (widget.detail.exercises.length).toString();
String weight = widget.detail.weight != null ? widget.detail.weight!.toStringAsFixed(1) : "-";
bool isDrop = false;
if (widget.detail.weight == -3) {
weight = t("DROP");
isDrop = true;
}
String restingTime = widget.detail.restingTime == null ? "" : widget.detail.restingTime!.toStringAsFixed(0);
bool isTest = false;
if (widget.detail.weight != null && widget.detail.weight! == -1) {
@ -574,6 +595,9 @@ class _ExerciseTileState extends State<ExerciseTile> with Trans {
if (widget.detail.repeats! == -1) {
repeats = t("MAX");
}
final bool extraExercise = widget.detail.exerciseType!.name == "Warming Up" || widget.detail.exerciseType!.name == "Stretching";
bool buddyWarning = widget.detail.exerciseType == null ? false : widget.detail.exerciseType!.buddyWarning;
setContext(context);
return Container(
color: Colors.transparent,
@ -620,10 +644,58 @@ class _ExerciseTileState extends State<ExerciseTile> with Trans {
Container(
width: 120,
height: 80,
child: Badge(
elevation: 0,
padding: EdgeInsets.all(0),
position: BadgePosition.bottomStart(start: -5),
animationDuration: Duration(milliseconds: 500),
animationType: BadgeAnimationType.slide,
badgeColor: Colors.transparent,
showBadge: true,
badgeContent: IconButton(
onPressed: () => showDialog(
context: context,
builder: (BuildContext context) {
return DialogHTML(
title: widget.detail.exerciseType!.nameTranslation,
htmlData: '<p>' + widget.detail.exerciseType!.descriptionTranslation + '</p>');
}),
icon: Icon(
Icons.info_outline,
color: Colors.yellow[200],
)),
child: Badge(
elevation: 0,
padding: EdgeInsets.all(0),
position: BadgePosition.topEnd(end: -8),
animationDuration: Duration(milliseconds: 500),
animationType: BadgeAnimationType.slide,
badgeColor: Colors.transparent,
showBadge: buddyWarning,
badgeContent: IconButton(
onPressed: () => showDialog(
context: context,
builder: (BuildContext context) {
return DialogCommon(
warning: true,
text: "Warning",
descriptions: t("Attention!"),
description2: t("The safe and exact execution of this exercise you need a training buddy or a trainer"),
description3: t("Execution at your own risk!"),
onTap: () => Navigator.of(context).pop(),
onCancel: () => Navigator.of(context).pop(),
title: t('Training Buddy'),
);
}),
icon: Icon(
CustomIcon.exclamation_circle,
color: Colors.red[800],
)),
child: MenuImage(
imageName: widget.bloc.getActualImageName(widget.detail.exerciseType!.exerciseTypeId),
workoutTreeId: widget.bloc.getActualWorkoutTreeId(widget.detail.exerciseType!.exerciseTypeId)!,
),
radius: 12,
))),
),
SizedBox(
width: 10,
@ -656,18 +728,18 @@ class _ExerciseTileState extends State<ExerciseTile> with Trans {
),
],
)),
widget.detail.exerciseType!.unitQuantityUnit != null
widget.detail.exerciseType!.unitQuantityUnit != null && !extraExercise
? TextSpan(
text: "\n",
)
: TextSpan(),
widget.detail.exerciseType!.unitQuantityUnit != null
widget.detail.exerciseType!.unitQuantityUnit != null && !extraExercise
? TextSpan(
text: t(widget.detail.exerciseType!.unitQuantityUnit!) + ": ",
style: GoogleFonts.inter(
fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold))
: TextSpan(),
widget.detail.exerciseType!.unitQuantityUnit != null
widget.detail.exerciseType!.unitQuantityUnit != null && !extraExercise
? TextSpan(
text: weight,
style: GoogleFonts.inter(
@ -677,37 +749,50 @@ class _ExerciseTileState extends State<ExerciseTile> with Trans {
TextSpan(
text: "\n",
),
TextSpan(
!extraExercise
? TextSpan(
text: t(widget.detail.exerciseType!.unit) + ": ",
style: GoogleFonts.inter(
fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)),
TextSpan(
fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold))
: TextSpan(),
!extraExercise
? TextSpan(
text: repeats,
style: GoogleFonts.inter(
fontSize: 12,
)),
))
: TextSpan(),
TextSpan(
text: "\n",
),
TextSpan(
!extraExercise
? TextSpan(
text: t("Set") + ": ",
style: GoogleFonts.inter(
fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)),
TextSpan(
fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold))
: TextSpan(),
!extraExercise
? TextSpan(
text: step + "/" + countSerie,
style: GoogleFonts.inter(
fontSize: 12,
)),
))
: TextSpan(),
TextSpan(
text: "\n",
),
TextSpan(
!extraExercise
? TextSpan(
text: t("Resting time") + ": ",
style: GoogleFonts.inter(
fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)),
TextSpan(
fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold))
: TextSpan(),
!extraExercise
? TextSpan(
text: restingTime + " " + t("min(s)"),
style: GoogleFonts.inter(fontSize: 12, color: done ? Colors.grey[100] : Colors.white, fontWeight: FontWeight.bold)),
style:
GoogleFonts.inter(fontSize: 12, color: done ? Colors.grey[100] : Colors.white, fontWeight: FontWeight.bold))
: TextSpan(),
]),
)),
isTest
@ -740,6 +825,34 @@ class _ExerciseTileState extends State<ExerciseTile> with Trans {
)),
]);
})
: isDrop
? AnimatedBuilder(
animation: animation,
builder: (context, snapshot) {
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
GestureDetector(
onTap: () => showDialog(
context: context,
builder: (BuildContext context) {
return DialogCommon(
warning: false,
title: t("Drop set"),
descriptions: t("Drop set"),
description2: t("Recommended method:"),
text: "OK",
onTap: () => Navigator.of(context).pop(),
onCancel: () => {
Navigator.of(context).pop(),
},
);
}),
child: Icon(
CustomIcon.question_circle,
color: Colors.orange[200],
size: 16,
)),
]);
})
: Offstage()
]),
),

View File

@ -6,7 +6,6 @@ import 'package:aitrainer_app/model/customer_training_plan_details.dart';
import 'package:aitrainer_app/util/trans.dart';
import 'package:aitrainer_app/widgets/app_bar.dart';
import 'package:aitrainer_app/widgets/exercise_save.dart';
import 'package:aitrainer_app/widgets/number_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
@ -43,7 +42,7 @@ class TrainingPlanExercise extends StatelessWidget with Trans {
}, builder: (context, state) {
return ModalProgressHUD(
child: getExercises(bloc, detail),
inAsyncCall: state is TrainingPlanLoading,
inAsyncCall: state is TrainingPlanExerciseLoading,
opacity: 0.5,
color: Colors.black54,
progressIndicator: CircularProgressIndicator(),
@ -52,6 +51,7 @@ class TrainingPlanExercise extends StatelessWidget with Trans {
),
floatingActionButton: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
FloatingActionButton.extended(
heroTag: "skipButton",
onPressed: () => {
Navigator.of(context).pop(),
bloc.add(TrainingPlanSkipExercise(detail: detail)),
@ -67,6 +67,7 @@ class TrainingPlanExercise extends StatelessWidget with Trans {
width: 20,
),
FloatingActionButton.extended(
heroTag: "saveButton",
onPressed: () => {
Navigator.of(context).pop(),
bloc.add(TrainingPlanSaveExercise(detail: detail)),
@ -111,7 +112,7 @@ class TrainingPlanExercise extends StatelessWidget with Trans {
);
}
Widget getExerciseForm(TrainingPlanBloc bloc, CustomerTrainingPlanDetails detail) {
/* Widget getExerciseForm(TrainingPlanBloc bloc, CustomerTrainingPlanDetails detail) {
return Container(
padding: const EdgeInsets.only(top: 10, left: 25, right: 25),
child: SingleChildScrollView(
@ -224,5 +225,5 @@ class TrainingPlanExercise extends StatelessWidget with Trans {
crossAxisAlignment: CrossAxisAlignment.start,
children: listWidgets,
);
}
} */
}

View File

@ -85,6 +85,7 @@ class MyTrainingPlans extends StatelessWidget with Trans, Logging {
getTrainingPlan(t("Training Plans for Women"), "asset/menu/training_plans_q_woman.jpg", "for_woman"),
getTrainingPlan(t("Training Plans of Celebrities"), "asset/menu/training_plans_q_celebrities.jpg", "celebrities"),
getTrainingPlan(t("Training Plans for Gain Strength"), "asset/menu/training_plans_q_gain_strength.jpg", "gain_strength"),
getTrainingPlan(t("Physical Prepare Program for Footgolfers"), "asset/menu/FG_2_edz.jpg", "footgolf"),
]),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:aitrainer_app/library/image_cache.dart' as wt;
import 'package:aitrainer_app/library/transparent_image.dart';
// ignore: must_be_immutable
class MenuImage extends StatelessWidget {
final int? workoutTreeId;
final String imageName;

View File

@ -25,10 +25,12 @@ class TreeviewParentWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget parentWidget = Text(
this.text,
style: GoogleFonts.archivoBlack(fontSize: fontSize, color: color ?? Colors.blue[800]!, backgroundColor: Colors.transparent),
);
Widget parentWidget = Text(this.text,
style: GoogleFonts.archivoBlack(
fontSize: fontSize,
color: color ?? Colors.blue[800]!,
backgroundColor: Colors.transparent,
));
return Card(
color: backgroundColor,

View File

@ -22,13 +22,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.6"
apple_sign_in:
dependency: "direct main"
description:
name: apple_sign_in
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
archive:
dependency: transitive
description:
@ -50,6 +43,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.6.1"
awesome_notifications:
dependency: "direct main"
description:
name: awesome_notifications
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.6+9"
badges:
dependency: "direct main"
description:
@ -442,13 +442,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.36.1"
flurry:
flurry_data:
dependency: "direct main"
description:
name: flurry
name: flurry_data
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.7"
version: "0.0.1"
flutter:
dependency: "direct main"
description: flutter
@ -517,20 +517,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0+4"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
@ -1064,6 +1050,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
sign_in_with_apple:
dependency: "direct main"
description:
name: sign_in_with_apple
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
@ -1216,13 +1209,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
timezone:
dependency: transitive
description:
name: timezone
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.0"
timing:
dependency: transitive
description:
@ -1435,4 +1421,4 @@ packages:
version: "3.1.0"
sdks:
dart: ">=2.12.0 <3.0.0"
flutter: ">=2.0.0"
flutter: ">=2.0.4"

View File

@ -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.19+89
version: 1.1.19+90
environment:
sdk: ">=2.12.0 <3.0.0"
@ -53,10 +53,8 @@ dependencies:
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
url_launcher: ^6.0.3
extended_tabs: ^2.2.0
upgrader: ^3.3.0
@ -64,20 +62,19 @@ dependencies:
firebase_core: ^1.2.0
firebase_analytics: ^8.1.0
firebase_messaging: ^10.0.0
flutter_local_notifications: ^5.0.0
firebase_auth: ^1.2.0
firebase_remote_config: ^0.10.0
awesome_notifications: ^0.0.6+9
syncfusion_flutter_gauges: ^19.1.63
syncfusion_flutter_datagrid: ^19.1.63
flutter_facebook_auth: ^3.4.0
google_sign_in: ^5.0.3
apple_sign_in: ^0.1.0
#sign_in_with_apple: ^3.0.0
sign_in_with_apple: ^3.0.0
#smartlook: ^1.0.7
flurry: ^0.0.4
flurry_data: ^0.0.1
flutter_uxcam: ^2.0.0-beta.1
animated_widgets: ^1.0.6
@ -249,6 +246,7 @@ flutter:
- asset/menu/400m.jpg
- asset/menu/FG_1_test.jpg
- asset/menu/FG_1_training.jpg
- asset/menu/FG_2_edz.jpg
- asset/menu/alternate_dumbbell_presses.jpg
- asset/menu/alternate_standing_shoulder_press.jpg
- asset/menu/arnold_press.jpg
@ -385,6 +383,7 @@ flutter:
- asset/menu/standing_triceps_extension.jpg
- asset/menu/stiff_legged_deadlift.jpg
- asset/menu/straight-arm_rope_pull-down.jpg
- asset/menu/stretching.jpg
- asset/menu/t_bar_rows.jpg
- asset/menu/test_center.jpg
- asset/menu/test_on_machines.jpg
@ -411,6 +410,7 @@ flutter:
- asset/menu/upper_body.jpg
- asset/menu/v_ups.jpg
- asset/menu/wall_sit.jpg
- asset/menu/warmup.jpg
- asset/menu/warrior_stand.jpg
- asset/menu/weight_free_test.jpg
- asset/menu/weighted_bench_dip.jpg