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(
|
||||
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: [
|
||||
// Splash / Initial Route
|
||||
GoRoute(
|
||||
|
|
|
|||
|
|
@ -356,6 +356,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
final key = settings['accessory_template'] as String?;
|
||||
if (key == 'hypertrophy') return AccessoryTemplate.hypertrophy;
|
||||
if (key == 'conditioning') return AccessoryTemplate.conditioning;
|
||||
if (key == 'journey_pullup') return AccessoryTemplate.journey_pullup;
|
||||
return AccessoryTemplate.none;
|
||||
}
|
||||
|
||||
|
|
@ -369,6 +370,9 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
if (newTemplate == AccessoryTemplate.conditioning) {
|
||||
templateKey = 'conditioning';
|
||||
}
|
||||
if (newTemplate == AccessoryTemplate.journey_pullup) {
|
||||
templateKey = 'journey_pullup';
|
||||
}
|
||||
|
||||
final currentSettings =
|
||||
Map<String, dynamic>.from(_user!.inventorySettings ?? {});
|
||||
|
|
@ -622,6 +626,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
final current = _getTemplateFromSettings(_user?.inventorySettings ?? {});
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_RadioTile<AccessoryTemplate>(
|
||||
value: AccessoryTemplate.none,
|
||||
|
|
@ -646,6 +651,24 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
subtitle: '15 min Kettlebell intervals to boost stamina.',
|
||||
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> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
bool _obscurePassword = true;
|
||||
bool _obscureConfirmPassword = true;
|
||||
// final _passwordController = TextEditingController();
|
||||
// final _confirmPasswordController = TextEditingController();
|
||||
// bool _obscurePassword = true;
|
||||
// bool _obscureConfirmPassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
// _passwordController.dispose();
|
||||
// _confirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
|||
|
||||
ref.read(onboardingDataProvider.notifier).updateData({
|
||||
'email': _emailController.text.trim(),
|
||||
'password': _passwordController.text,
|
||||
// 'password': _passwordController.text,
|
||||
});
|
||||
|
||||
context.go('/onboarding/welcome');
|
||||
|
|
@ -87,60 +87,60 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
|||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _obscurePassword = !_obscurePassword);
|
||||
},
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter a password';
|
||||
}
|
||||
if (value.length < 8) {
|
||||
return 'Password must be at least 8 characters';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _confirmPasswordController,
|
||||
obscureText: _obscureConfirmPassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Confirm Password',
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureConfirmPassword
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _obscureConfirmPassword =
|
||||
!_obscureConfirmPassword);
|
||||
},
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value != _passwordController.text) {
|
||||
return 'Passwords do not match';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
// const SizedBox(height: 16),
|
||||
// TextFormField(
|
||||
// controller: _passwordController,
|
||||
// obscureText: _obscurePassword,
|
||||
// decoration: InputDecoration(
|
||||
// labelText: 'Password',
|
||||
// prefixIcon: const Icon(Icons.lock_outline),
|
||||
// suffixIcon: IconButton(
|
||||
// icon: Icon(
|
||||
// _obscurePassword
|
||||
// ? Icons.visibility_outlined
|
||||
// : Icons.visibility_off_outlined,
|
||||
// ),
|
||||
// onPressed: () {
|
||||
// setState(() => _obscurePassword = !_obscurePassword);
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// validator: (value) {
|
||||
// if (value == null || value.isEmpty) {
|
||||
// return 'Please enter a password';
|
||||
// }
|
||||
// if (value.length < 8) {
|
||||
// return 'Password must be at least 8 characters';
|
||||
// }
|
||||
// return null;
|
||||
// },
|
||||
// ),
|
||||
// const SizedBox(height: 16),
|
||||
// TextFormField(
|
||||
// controller: _confirmPasswordController,
|
||||
// obscureText: _obscureConfirmPassword,
|
||||
// decoration: InputDecoration(
|
||||
// labelText: 'Confirm Password',
|
||||
// prefixIcon: const Icon(Icons.lock_outline),
|
||||
// suffixIcon: IconButton(
|
||||
// icon: Icon(
|
||||
// _obscureConfirmPassword
|
||||
// ? Icons.visibility_outlined
|
||||
// : Icons.visibility_off_outlined,
|
||||
// ),
|
||||
// onPressed: () {
|
||||
// setState(() => _obscureConfirmPassword =
|
||||
// !_obscureConfirmPassword);
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// validator: (value) {
|
||||
// if (value != _passwordController.text) {
|
||||
// return 'Passwords do not match';
|
||||
// }
|
||||
// return null;
|
||||
// },
|
||||
// ),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
onPressed: _handleRegister,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
|
@ -23,71 +25,96 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
|
|||
bool _isLoading = false;
|
||||
|
||||
Future<void> _handleFinish() async {
|
||||
final password = await _showPasswordDialog();
|
||||
if (password == null) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
final onboardingData = ref.read(onboardingDataProvider);
|
||||
final userRepo = ref.read(userRepositoryProvider);
|
||||
final inventorySettings =
|
||||
onboardingData['inventory_settings'] as Map<String, dynamic>;
|
||||
(onboardingData['inventory_settings'] as Map<String, dynamic>?) ?? {};
|
||||
|
||||
final exerciseVariants =
|
||||
onboardingData['exercise_variants'] as Map<String, dynamic>?;
|
||||
var user = await userRepo.getLocalUser();
|
||||
|
||||
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(
|
||||
email: onboardingData['email'] ?? '',
|
||||
password: onboardingData['password'] ?? '',
|
||||
bodyweight: onboardingData['bodyweight'] ?? 80.0,
|
||||
email: email,
|
||||
password: password,
|
||||
bodyweight: bodyweight,
|
||||
inventorySettings: inventorySettings,
|
||||
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 {
|
||||
user = user.copyWith(
|
||||
currentBodyweight:
|
||||
onboardingData['bodyweight'] ?? user.currentBodyweight,
|
||||
(onboardingData['bodyweight'] as num?)?.toDouble() ??
|
||||
user.currentBodyweight,
|
||||
inventorySettings: Value(inventorySettings),
|
||||
isDirty: true,
|
||||
);
|
||||
await userRepo.saveLocalUser(user);
|
||||
|
||||
try {
|
||||
await userRepo.updateBodyweight(user.currentBodyweight);
|
||||
await userRepo.updateInventory(inventorySettings);
|
||||
} catch (e) {
|
||||
// Sync macht das später
|
||||
}
|
||||
}
|
||||
|
||||
final avatarJson = _config.toJson();
|
||||
|
||||
user = user.copyWith(
|
||||
avatarConfig: Value(_config.toJson()),
|
||||
avatarConfig: Value(avatarJson),
|
||||
isDirty: true,
|
||||
);
|
||||
await userRepo.saveLocalUser(user);
|
||||
try {
|
||||
final trainingMaxes =
|
||||
onboardingData['training_maxes'] as Map<String, dynamic>?;
|
||||
|
||||
final trainingMaxes =
|
||||
onboardingData['training_maxes'] as Map<String, dynamic>?;
|
||||
if (trainingMaxes != null) {
|
||||
final cycleRepo = ref.read(cycleRepositoryProvider);
|
||||
final tmMap = <String, double>{
|
||||
'squat': (trainingMaxes['squat'] as num?)?.toDouble() ?? 100.0,
|
||||
'pullup': (trainingMaxes['pullup'] as num?)?.toDouble() ?? 80.0,
|
||||
'dip': (trainingMaxes['dip'] as num?)?.toDouble() ?? 90.0,
|
||||
};
|
||||
await cycleRepo.createCycle(tmMap);
|
||||
if (trainingMaxes != null && trainingMaxes.isNotEmpty) {
|
||||
final cycleRepo = ref.read(cycleRepositoryProvider);
|
||||
|
||||
final tmMap = <String, double>{
|
||||
'squat': (trainingMaxes['squat'] as num?)?.toDouble() ?? 100.0,
|
||||
'pullup': (trainingMaxes['pullup'] as num?)?.toDouble() ?? 80.0,
|
||||
'dip': (trainingMaxes['dip'] as num?)?.toDouble() ?? 90.0,
|
||||
};
|
||||
|
||||
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) {
|
||||
ref.read(onboardingDataProvider.notifier).state = {};
|
||||
ref.read(onboardingDataProvider.notifier).clear();
|
||||
|
||||
context.go('/hub');
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e, stackTrace) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Setup failed: $e'),
|
||||
backgroundColor: AppTheme.errorColor),
|
||||
content: Text('Setup failed: $e'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
}
|
||||
} 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';
|
||||
|
||||
// Provider to store onboarding data
|
||||
@riverpod
|
||||
@Riverpod(keepAlive: true)
|
||||
class OnboardingData extends _$OnboardingData {
|
||||
@override
|
||||
Map<String, dynamic> build() => {};
|
||||
|
||||
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) {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ class WorkoutGeneratorService {
|
|||
? conditioningSets
|
||||
: 15;
|
||||
exercises.addAll(_generateConditioning(day, sets));
|
||||
} else if (template == AccessoryTemplate.journey_pullup) {
|
||||
exercises.addAll(_generatePullUpJourney(day, trainingMaxes));
|
||||
}
|
||||
|
||||
return exercises;
|
||||
|
|
@ -187,6 +189,68 @@ class WorkoutGeneratorService {
|
|||
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) {
|
||||
final accessories = <Exercise>[];
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ class ApiClient {
|
|||
final FlutterSecureStorage _storage;
|
||||
final Logger _logger;
|
||||
|
||||
bool _isRefreshing = false;
|
||||
final List<Function> _requestsQueue = [];
|
||||
|
||||
ApiClient({
|
||||
FlutterSecureStorage? storage,
|
||||
Logger? logger,
|
||||
|
|
@ -49,15 +52,117 @@ class ApiClient {
|
|||
},
|
||||
onError: (error, handler) async {
|
||||
if (error.response?.statusCode == 401) {
|
||||
_logger.w('Unauthorized - clearing token');
|
||||
await _storage.delete(key: AppConstants.keyAuthToken);
|
||||
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);
|
||||
_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);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
|
|
|
|||
|
|
@ -22,10 +22,15 @@ enum ExerciseType {
|
|||
kb_swing,
|
||||
kb_snatch,
|
||||
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 {
|
||||
static const Map<int, List<double>> weekPercentages = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue