feat: added pullup journey
This commit is contained in:
parent
9ebc70ad27
commit
a5efbf8dad
8 changed files with 406 additions and 95 deletions
|
|
@ -25,6 +25,22 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||||
|
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
initialLocation: '/splash',
|
initialLocation: '/splash',
|
||||||
|
redirect: (context, state) async {
|
||||||
|
final user = await userRepo.getLocalUser();
|
||||||
|
final isAuthenticated = user != null;
|
||||||
|
|
||||||
|
final isOnAuthPage = state.matchedLocation == '/login' ||
|
||||||
|
state.matchedLocation == '/register' ||
|
||||||
|
state.matchedLocation.startsWith('/onboarding');
|
||||||
|
|
||||||
|
if (!isAuthenticated &&
|
||||||
|
!isOnAuthPage &&
|
||||||
|
state.matchedLocation != '/splash') {
|
||||||
|
return '/login';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
// Splash / Initial Route
|
// Splash / Initial Route
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
final key = settings['accessory_template'] as String?;
|
final key = settings['accessory_template'] as String?;
|
||||||
if (key == 'hypertrophy') return AccessoryTemplate.hypertrophy;
|
if (key == 'hypertrophy') return AccessoryTemplate.hypertrophy;
|
||||||
if (key == 'conditioning') return AccessoryTemplate.conditioning;
|
if (key == 'conditioning') return AccessoryTemplate.conditioning;
|
||||||
|
if (key == 'journey_pullup') return AccessoryTemplate.journey_pullup;
|
||||||
return AccessoryTemplate.none;
|
return AccessoryTemplate.none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -369,6 +370,9 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
if (newTemplate == AccessoryTemplate.conditioning) {
|
if (newTemplate == AccessoryTemplate.conditioning) {
|
||||||
templateKey = 'conditioning';
|
templateKey = 'conditioning';
|
||||||
}
|
}
|
||||||
|
if (newTemplate == AccessoryTemplate.journey_pullup) {
|
||||||
|
templateKey = 'journey_pullup';
|
||||||
|
}
|
||||||
|
|
||||||
final currentSettings =
|
final currentSettings =
|
||||||
Map<String, dynamic>.from(_user!.inventorySettings ?? {});
|
Map<String, dynamic>.from(_user!.inventorySettings ?? {});
|
||||||
|
|
@ -622,6 +626,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
final current = _getTemplateFromSettings(_user?.inventorySettings ?? {});
|
final current = _getTemplateFromSettings(_user?.inventorySettings ?? {});
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_RadioTile<AccessoryTemplate>(
|
_RadioTile<AccessoryTemplate>(
|
||||||
value: AccessoryTemplate.none,
|
value: AccessoryTemplate.none,
|
||||||
|
|
@ -646,6 +651,24 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
subtitle: '15 min Kettlebell intervals to boost stamina.',
|
subtitle: '15 min Kettlebell intervals to boost stamina.',
|
||||||
onChanged: (val) => _updateTemplate(val!),
|
onChanged: (val) => _updateTemplate(val!),
|
||||||
),
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 24, 16, 8),
|
||||||
|
child: Text(
|
||||||
|
'ACTIVE JOURNEYS',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_RadioTile<AccessoryTemplate>(
|
||||||
|
value: AccessoryTemplate.journey_pullup,
|
||||||
|
groupValue: current,
|
||||||
|
title: 'Quest: The First Pull-Up',
|
||||||
|
subtitle: 'Specific progression to master your bodyweight.',
|
||||||
|
onChanged: (val) => _updateTemplate(val!),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,16 @@ class RegisterScreen extends ConsumerStatefulWidget {
|
||||||
class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final _emailController = TextEditingController();
|
final _emailController = TextEditingController();
|
||||||
final _passwordController = TextEditingController();
|
// final _passwordController = TextEditingController();
|
||||||
final _confirmPasswordController = TextEditingController();
|
// final _confirmPasswordController = TextEditingController();
|
||||||
bool _obscurePassword = true;
|
// bool _obscurePassword = true;
|
||||||
bool _obscureConfirmPassword = true;
|
// bool _obscureConfirmPassword = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_emailController.dispose();
|
_emailController.dispose();
|
||||||
_passwordController.dispose();
|
// _passwordController.dispose();
|
||||||
_confirmPasswordController.dispose();
|
// _confirmPasswordController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||||
|
|
||||||
ref.read(onboardingDataProvider.notifier).updateData({
|
ref.read(onboardingDataProvider.notifier).updateData({
|
||||||
'email': _emailController.text.trim(),
|
'email': _emailController.text.trim(),
|
||||||
'password': _passwordController.text,
|
// 'password': _passwordController.text,
|
||||||
});
|
});
|
||||||
|
|
||||||
context.go('/onboarding/welcome');
|
context.go('/onboarding/welcome');
|
||||||
|
|
@ -87,60 +87,60 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
// const SizedBox(height: 16),
|
||||||
TextFormField(
|
// TextFormField(
|
||||||
controller: _passwordController,
|
// controller: _passwordController,
|
||||||
obscureText: _obscurePassword,
|
// obscureText: _obscurePassword,
|
||||||
decoration: InputDecoration(
|
// decoration: InputDecoration(
|
||||||
labelText: 'Password',
|
// labelText: 'Password',
|
||||||
prefixIcon: const Icon(Icons.lock_outline),
|
// prefixIcon: const Icon(Icons.lock_outline),
|
||||||
suffixIcon: IconButton(
|
// suffixIcon: IconButton(
|
||||||
icon: Icon(
|
// icon: Icon(
|
||||||
_obscurePassword
|
// _obscurePassword
|
||||||
? Icons.visibility_outlined
|
// ? Icons.visibility_outlined
|
||||||
: Icons.visibility_off_outlined,
|
// : Icons.visibility_off_outlined,
|
||||||
),
|
// ),
|
||||||
onPressed: () {
|
// onPressed: () {
|
||||||
setState(() => _obscurePassword = !_obscurePassword);
|
// setState(() => _obscurePassword = !_obscurePassword);
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
validator: (value) {
|
// validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
// if (value == null || value.isEmpty) {
|
||||||
return 'Please enter a password';
|
// return 'Please enter a password';
|
||||||
}
|
// }
|
||||||
if (value.length < 8) {
|
// if (value.length < 8) {
|
||||||
return 'Password must be at least 8 characters';
|
// return 'Password must be at least 8 characters';
|
||||||
}
|
// }
|
||||||
return null;
|
// return null;
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
const SizedBox(height: 16),
|
// const SizedBox(height: 16),
|
||||||
TextFormField(
|
// TextFormField(
|
||||||
controller: _confirmPasswordController,
|
// controller: _confirmPasswordController,
|
||||||
obscureText: _obscureConfirmPassword,
|
// obscureText: _obscureConfirmPassword,
|
||||||
decoration: InputDecoration(
|
// decoration: InputDecoration(
|
||||||
labelText: 'Confirm Password',
|
// labelText: 'Confirm Password',
|
||||||
prefixIcon: const Icon(Icons.lock_outline),
|
// prefixIcon: const Icon(Icons.lock_outline),
|
||||||
suffixIcon: IconButton(
|
// suffixIcon: IconButton(
|
||||||
icon: Icon(
|
// icon: Icon(
|
||||||
_obscureConfirmPassword
|
// _obscureConfirmPassword
|
||||||
? Icons.visibility_outlined
|
// ? Icons.visibility_outlined
|
||||||
: Icons.visibility_off_outlined,
|
// : Icons.visibility_off_outlined,
|
||||||
),
|
// ),
|
||||||
onPressed: () {
|
// onPressed: () {
|
||||||
setState(() => _obscureConfirmPassword =
|
// setState(() => _obscureConfirmPassword =
|
||||||
!_obscureConfirmPassword);
|
// !_obscureConfirmPassword);
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
validator: (value) {
|
// validator: (value) {
|
||||||
if (value != _passwordController.text) {
|
// if (value != _passwordController.text) {
|
||||||
return 'Passwords do not match';
|
// return 'Passwords do not match';
|
||||||
}
|
// }
|
||||||
return null;
|
// return null;
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _handleRegister,
|
onPressed: _handleRegister,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
@ -23,71 +25,96 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
Future<void> _handleFinish() async {
|
Future<void> _handleFinish() async {
|
||||||
|
final password = await _showPasswordDialog();
|
||||||
|
if (password == null) return;
|
||||||
|
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final onboardingData = ref.read(onboardingDataProvider);
|
final onboardingData = ref.read(onboardingDataProvider);
|
||||||
final userRepo = ref.read(userRepositoryProvider);
|
final userRepo = ref.read(userRepositoryProvider);
|
||||||
final inventorySettings =
|
final inventorySettings =
|
||||||
onboardingData['inventory_settings'] as Map<String, dynamic>;
|
(onboardingData['inventory_settings'] as Map<String, dynamic>?) ?? {};
|
||||||
|
|
||||||
final exerciseVariants =
|
final exerciseVariants =
|
||||||
onboardingData['exercise_variants'] as Map<String, dynamic>?;
|
onboardingData['exercise_variants'] as Map<String, dynamic>?;
|
||||||
var user = await userRepo.getLocalUser();
|
var user = await userRepo.getLocalUser();
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
final email = onboardingData['email'] as String? ?? '';
|
||||||
|
final bodyweight =
|
||||||
|
(onboardingData['bodyweight'] as num?)?.toDouble() ?? 80.0;
|
||||||
|
|
||||||
|
if (email.isEmpty || password.isEmpty) {
|
||||||
|
throw Exception('Email or password is missing!');
|
||||||
|
}
|
||||||
|
|
||||||
user = await userRepo.register(
|
user = await userRepo.register(
|
||||||
email: onboardingData['email'] ?? '',
|
email: email,
|
||||||
password: onboardingData['password'] ?? '',
|
password: password,
|
||||||
bodyweight: onboardingData['bodyweight'] ?? 80.0,
|
bodyweight: bodyweight,
|
||||||
inventorySettings: inventorySettings,
|
inventorySettings: inventorySettings,
|
||||||
exerciseVariants: exerciseVariants,
|
exerciseVariants: exerciseVariants,
|
||||||
);
|
);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
user = await userRepo.getLocalUser();
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception(
|
||||||
|
'User registration succeeded but user not found in DB');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
user = user.copyWith(
|
user = user.copyWith(
|
||||||
currentBodyweight:
|
currentBodyweight:
|
||||||
onboardingData['bodyweight'] ?? user.currentBodyweight,
|
(onboardingData['bodyweight'] as num?)?.toDouble() ??
|
||||||
|
user.currentBodyweight,
|
||||||
inventorySettings: Value(inventorySettings),
|
inventorySettings: Value(inventorySettings),
|
||||||
isDirty: true,
|
isDirty: true,
|
||||||
);
|
);
|
||||||
await userRepo.saveLocalUser(user);
|
await userRepo.saveLocalUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
final avatarJson = _config.toJson();
|
||||||
await userRepo.updateBodyweight(user.currentBodyweight);
|
|
||||||
await userRepo.updateInventory(inventorySettings);
|
|
||||||
} catch (e) {
|
|
||||||
// Sync macht das später
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user = user.copyWith(
|
user = user.copyWith(
|
||||||
avatarConfig: Value(_config.toJson()),
|
avatarConfig: Value(avatarJson),
|
||||||
isDirty: true,
|
isDirty: true,
|
||||||
);
|
);
|
||||||
await userRepo.saveLocalUser(user);
|
await userRepo.saveLocalUser(user);
|
||||||
|
try {
|
||||||
final trainingMaxes =
|
final trainingMaxes =
|
||||||
onboardingData['training_maxes'] as Map<String, dynamic>?;
|
onboardingData['training_maxes'] as Map<String, dynamic>?;
|
||||||
if (trainingMaxes != null) {
|
|
||||||
|
if (trainingMaxes != null && trainingMaxes.isNotEmpty) {
|
||||||
final cycleRepo = ref.read(cycleRepositoryProvider);
|
final cycleRepo = ref.read(cycleRepositoryProvider);
|
||||||
|
|
||||||
final tmMap = <String, double>{
|
final tmMap = <String, double>{
|
||||||
'squat': (trainingMaxes['squat'] as num?)?.toDouble() ?? 100.0,
|
'squat': (trainingMaxes['squat'] as num?)?.toDouble() ?? 100.0,
|
||||||
'pullup': (trainingMaxes['pullup'] as num?)?.toDouble() ?? 80.0,
|
'pullup': (trainingMaxes['pullup'] as num?)?.toDouble() ?? 80.0,
|
||||||
'dip': (trainingMaxes['dip'] as num?)?.toDouble() ?? 90.0,
|
'dip': (trainingMaxes['dip'] as num?)?.toDouble() ?? 90.0,
|
||||||
};
|
};
|
||||||
await cycleRepo.createCycle(tmMap);
|
|
||||||
|
final cycle = await cycleRepo.createCycle(tmMap);
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
log('❌ CYCLE ERROR (non-critical): $e');
|
||||||
|
log(' Error type: ${e.runtimeType}');
|
||||||
|
log(' Stack:\n$stackTrace');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ref.read(onboardingDataProvider.notifier).state = {};
|
ref.read(onboardingDataProvider.notifier).clear();
|
||||||
|
|
||||||
context.go('/hub');
|
context.go('/hub');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, stackTrace) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Setup failed: $e'),
|
content: Text('Setup failed: $e'),
|
||||||
backgroundColor: AppTheme.errorColor),
|
backgroundColor: AppTheme.errorColor,
|
||||||
|
duration: const Duration(seconds: 5),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -137,4 +164,65 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> _showPasswordDialog() async {
|
||||||
|
final passwordController = TextEditingController();
|
||||||
|
final confirmController = TextEditingController();
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
return showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Secure Your Account'),
|
||||||
|
content: Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text('Choose a strong password to protect your progress'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: passwordController,
|
||||||
|
obscureText: true,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Password',
|
||||||
|
prefixIcon: Icon(Icons.lock),
|
||||||
|
),
|
||||||
|
validator: (v) =>
|
||||||
|
(v?.length ?? 0) < 8 ? 'Min 8 characters' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: confirmController,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Confirm Password',
|
||||||
|
prefixIcon: Icon(Icons.lock_outline),
|
||||||
|
),
|
||||||
|
validator: (v) => v != passwordController.text
|
||||||
|
? 'Passwords do not match'
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('CANCEL'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (formKey.currentState!.validate()) {
|
||||||
|
Navigator.pop(context, passwordController.text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('CONFIRM'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,19 +9,29 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
part 'bodyweight_input_screen.g.dart';
|
part 'bodyweight_input_screen.g.dart';
|
||||||
|
|
||||||
// Provider to store onboarding data
|
// Provider to store onboarding data
|
||||||
@riverpod
|
@Riverpod(keepAlive: true)
|
||||||
class OnboardingData extends _$OnboardingData {
|
class OnboardingData extends _$OnboardingData {
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> build() => {};
|
Map<String, dynamic> build() => {};
|
||||||
|
|
||||||
void update(Map<String, dynamic> Function(Map<String, dynamic> state) cb) {
|
void update(Map<String, dynamic> Function(Map<String, dynamic> state) cb) {
|
||||||
state = cb(state);
|
final newData = cb(state);
|
||||||
|
state = {...state, ...newData};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Eigene Methode für Updates hinzufügen, um die Syntax im UI sauber zu halten
|
|
||||||
void updateData(Map<String, dynamic> newValue) {
|
void updateData(Map<String, dynamic> newValue) {
|
||||||
state = {...state, ...newValue};
|
state = {...state, ...newValue};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeKey(String key) {
|
||||||
|
final newState = Map<String, dynamic>.from(state);
|
||||||
|
newState.remove(key);
|
||||||
|
state = newState;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BodyweightInputScreen extends ConsumerStatefulWidget {
|
class BodyweightInputScreen extends ConsumerStatefulWidget {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ class WorkoutGeneratorService {
|
||||||
? conditioningSets
|
? conditioningSets
|
||||||
: 15;
|
: 15;
|
||||||
exercises.addAll(_generateConditioning(day, sets));
|
exercises.addAll(_generateConditioning(day, sets));
|
||||||
|
} else if (template == AccessoryTemplate.journey_pullup) {
|
||||||
|
exercises.addAll(_generatePullUpJourney(day, trainingMaxes));
|
||||||
}
|
}
|
||||||
|
|
||||||
return exercises;
|
return exercises;
|
||||||
|
|
@ -187,6 +189,68 @@ class WorkoutGeneratorService {
|
||||||
return accessories;
|
return accessories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Exercise> _generatePullUpJourney(
|
||||||
|
int day, Map<String, double> trainingMaxes) {
|
||||||
|
final exercises = <Exercise>[];
|
||||||
|
|
||||||
|
Exercise createAccessory(
|
||||||
|
String id, String name, ExerciseType type, int sets, int reps,
|
||||||
|
{double weight = 0.0}) {
|
||||||
|
return Exercise(
|
||||||
|
exerciseId: id,
|
||||||
|
exerciseName: name,
|
||||||
|
bodyweightAtSession: 0,
|
||||||
|
sets: List.generate(
|
||||||
|
sets,
|
||||||
|
(i) => WorkoutSet(
|
||||||
|
setNumber: i + 1,
|
||||||
|
repsTarget: reps,
|
||||||
|
targetWeightTotal: weight,
|
||||||
|
repsActual: 0,
|
||||||
|
isAmrap: false,
|
||||||
|
completed: false,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double calculateWeight(double referenceTm, double percentage) {
|
||||||
|
final raw = referenceTm * percentage;
|
||||||
|
return (raw / 2.5).round() * 2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (day) {
|
||||||
|
case 1:
|
||||||
|
exercises.add(createAccessory('scap_pull', 'Scapular Pull-Ups',
|
||||||
|
ExerciseType.scapular_pull, 3, 10));
|
||||||
|
|
||||||
|
exercises.add(createAccessory(
|
||||||
|
'plank', 'Core Plank (45s)', ExerciseType.plank, 3, 1));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
exercises.add(createAccessory(
|
||||||
|
'inv_row', 'Australian Pull-Ups', ExerciseType.inverted_row, 4, 8));
|
||||||
|
|
||||||
|
exercises.add(createAccessory(
|
||||||
|
'face_pull', 'Band Face Pull', ExerciseType.face_pull, 3, 15));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
exercises.add(createAccessory('neg_pull', 'Negative Pull-Ups (5s slow)',
|
||||||
|
ExerciseType.negative_pullup, 3, 4));
|
||||||
|
|
||||||
|
final rowTm = trainingMaxes['row'] ?? 0.0;
|
||||||
|
final curlWeight = rowTm > 0 ? calculateWeight(rowTm, 0.3) : 0.0;
|
||||||
|
|
||||||
|
exercises.add(createAccessory(
|
||||||
|
'curl', 'Barbell Curl', ExerciseType.curl_barbell, 3, 10,
|
||||||
|
weight: curlWeight));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exercises;
|
||||||
|
}
|
||||||
|
|
||||||
List<Exercise> _generateConditioning(int day, int targetSets) {
|
List<Exercise> _generateConditioning(int day, int targetSets) {
|
||||||
final accessories = <Exercise>[];
|
final accessories = <Exercise>[];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ class ApiClient {
|
||||||
final FlutterSecureStorage _storage;
|
final FlutterSecureStorage _storage;
|
||||||
final Logger _logger;
|
final Logger _logger;
|
||||||
|
|
||||||
|
bool _isRefreshing = false;
|
||||||
|
final List<Function> _requestsQueue = [];
|
||||||
|
|
||||||
ApiClient({
|
ApiClient({
|
||||||
FlutterSecureStorage? storage,
|
FlutterSecureStorage? storage,
|
||||||
Logger? logger,
|
Logger? logger,
|
||||||
|
|
@ -49,15 +52,117 @@ class ApiClient {
|
||||||
},
|
},
|
||||||
onError: (error, handler) async {
|
onError: (error, handler) async {
|
||||||
if (error.response?.statusCode == 401) {
|
if (error.response?.statusCode == 401) {
|
||||||
_logger.w('Unauthorized - clearing token');
|
final token = await _storage.read(key: AppConstants.keyAuthToken);
|
||||||
|
|
||||||
|
if (token != null && !_isRefreshing) {
|
||||||
|
_isRefreshing = true;
|
||||||
|
_logger.w('🔄 Token expired, attempting refresh...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final newToken = await refreshToken();
|
||||||
|
|
||||||
|
if (newToken != null) {
|
||||||
|
error.requestOptions.headers['Authorization'] =
|
||||||
|
'Bearer $newToken';
|
||||||
|
|
||||||
|
final response = await _dio.fetch(error.requestOptions);
|
||||||
|
_isRefreshing = false;
|
||||||
|
|
||||||
|
_processQueue(newToken);
|
||||||
|
|
||||||
|
return handler.resolve(response);
|
||||||
|
} else {
|
||||||
|
_logger.e('❌ Token refresh failed - logging out');
|
||||||
await _storage.delete(key: AppConstants.keyAuthToken);
|
await _storage.delete(key: AppConstants.keyAuthToken);
|
||||||
|
_isRefreshing = false;
|
||||||
|
_clearQueue();
|
||||||
|
return handler.next(error);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_logger.e('❌ Refresh error: $e');
|
||||||
|
await _storage.delete(key: AppConstants.keyAuthToken);
|
||||||
|
_isRefreshing = false;
|
||||||
|
_clearQueue();
|
||||||
|
return handler.next(error);
|
||||||
|
}
|
||||||
|
} else if (_isRefreshing) {
|
||||||
|
_logger.i('⏳ Waiting for token refresh...');
|
||||||
|
return _queueRequest(() async {
|
||||||
|
final newToken =
|
||||||
|
await _storage.read(key: AppConstants.keyAuthToken);
|
||||||
|
if (newToken != null) {
|
||||||
|
error.requestOptions.headers['Authorization'] =
|
||||||
|
'Bearer $newToken';
|
||||||
|
return await _dio.fetch(error.requestOptions);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}, handler);
|
||||||
|
} else {
|
||||||
|
await _storage.delete(key: AppConstants.keyAuthToken);
|
||||||
|
return handler.next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// onError: (error, handler) async {
|
||||||
|
// if (error.response?.statusCode == 401) {
|
||||||
|
// _logger.w('Unauthorized - clearing token');
|
||||||
|
// await _storage.delete(key: AppConstants.keyAuthToken);
|
||||||
|
// }
|
||||||
return handler.next(error);
|
return handler.next(error);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _queueRequest(
|
||||||
|
Future<Response> Function() request,
|
||||||
|
ErrorInterceptorHandler handler,
|
||||||
|
) async {
|
||||||
|
_requestsQueue.add(() async {
|
||||||
|
try {
|
||||||
|
final response = await request();
|
||||||
|
handler.resolve(response);
|
||||||
|
} catch (e) {
|
||||||
|
handler.reject(e as DioException);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _processQueue(String newToken) {
|
||||||
|
for (final request in _requestsQueue) {
|
||||||
|
request();
|
||||||
|
}
|
||||||
|
_requestsQueue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearQueue() {
|
||||||
|
_requestsQueue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> refreshToken() async {
|
||||||
|
try {
|
||||||
|
final token = await _storage.read(key: AppConstants.keyAuthToken);
|
||||||
|
if (token == null) return null;
|
||||||
|
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/collections/users/auth-refresh',
|
||||||
|
options: Options(
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final newToken = response.data['token'];
|
||||||
|
if (newToken != null) {
|
||||||
|
await _storage.write(key: AppConstants.keyAuthToken, value: newToken);
|
||||||
|
_logger.i('✅ Token refreshed successfully');
|
||||||
|
return newToken;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
_logger.e('❌ Token refresh failed', error: e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> login(String email, String password) async {
|
Future<Map<String, dynamic>> login(String email, String password) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,15 @@ enum ExerciseType {
|
||||||
kb_swing,
|
kb_swing,
|
||||||
kb_snatch,
|
kb_snatch,
|
||||||
kb_thruster,
|
kb_thruster,
|
||||||
kb_clean_press
|
kb_clean_press,
|
||||||
|
|
||||||
|
// pullup journey
|
||||||
|
scapular_pull,
|
||||||
|
inverted_row,
|
||||||
|
negative_pullup,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AccessoryTemplate { none, hypertrophy, conditioning }
|
enum AccessoryTemplate { none, hypertrophy, conditioning, journey_pullup }
|
||||||
|
|
||||||
class WendlerCalculator {
|
class WendlerCalculator {
|
||||||
static const Map<int, List<double>> weekPercentages = {
|
static const Map<int, List<double>> weekPercentages = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue