feat: add assisted exercise an next set preview
This commit is contained in:
parent
44f5703de4
commit
79a7e1c50d
10 changed files with 534 additions and 270 deletions
|
|
@ -10,11 +10,11 @@ void main() async {
|
|||
|
||||
try {
|
||||
await dotenv.load(fileName: '.env');
|
||||
debugPrint('✅ Environment loaded: ${dotenv.env['ENVIRONMENT']}');
|
||||
debugPrint('✅ API URL: ${dotenv.env['API_BASE_URL']}');
|
||||
debugPrint('Environment loaded: ${dotenv.env['ENVIRONMENT']}');
|
||||
debugPrint('API URL: ${dotenv.env['API_BASE_URL']}');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Could not load .env file: $e');
|
||||
debugPrint('⚠️ Using default production values');
|
||||
debugPrint('Could not load .env file: $e');
|
||||
debugPrint('Using default production values');
|
||||
}
|
||||
|
||||
await SystemChrome.setPreferredOrientations([
|
||||
|
|
|
|||
|
|
@ -425,199 +425,204 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
AvatarRenderer(
|
||||
config: avatarConfig,
|
||||
size: 120,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: AppTheme.surfaceColor,
|
||||
radius: 18,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.edit, size: 16),
|
||||
onPressed: _showAvatarEditor,
|
||||
: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
AvatarRenderer(
|
||||
config: avatarConfig,
|
||||
size: 120,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: AppTheme.surfaceColor,
|
||||
radius: 18,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.edit, size: 16),
|
||||
onPressed: _showAvatarEditor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Center(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _showBackgroundSelector,
|
||||
icon: const Icon(Icons.landscape),
|
||||
label: const Text('CHANGE SCENERY'),
|
||||
const SizedBox(height: 32),
|
||||
Center(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _showBackgroundSelector,
|
||||
icon: const Icon(Icons.landscape),
|
||||
label: const Text('CHANGE SCENERY'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text('Physical Stats',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(color: AppTheme.textPrimary)),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Current Bodyweight',
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: _currentBodyweight,
|
||||
min: 40,
|
||||
max: 150,
|
||||
divisions: 220,
|
||||
label: _currentBodyweight.toStringAsFixed(1),
|
||||
activeColor: AppTheme.primaryColor,
|
||||
onChanged: (val) => setState(() {
|
||||
_currentBodyweight = val;
|
||||
_hasChanges = true;
|
||||
}),
|
||||
const SizedBox(height: 32),
|
||||
Text('Physical Stats',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(color: AppTheme.textPrimary)),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Current Bodyweight',
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: _currentBodyweight,
|
||||
min: 40,
|
||||
max: 150,
|
||||
divisions: 220,
|
||||
label: _currentBodyweight.toStringAsFixed(1),
|
||||
activeColor: AppTheme.primaryColor,
|
||||
onChanged: (val) => setState(() {
|
||||
_currentBodyweight = val;
|
||||
_hasChanges = true;
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${_currentBodyweight.toStringAsFixed(1)} kg',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text('Training Focus',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(color: AppTheme.textPrimary)),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Accessory Template',
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
const SizedBox(height: 12),
|
||||
_buildTemplateSelector(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('Account Security',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(color: AppTheme.textPrimary)),
|
||||
const SizedBox(height: 8),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.lock_outline),
|
||||
title: const Text('Change Password'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: _showChangePasswordDialog,
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(height: 24),
|
||||
Text('Danger Zone',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(color: AppTheme.errorColor)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: AppTheme.errorColor.withValues(alpha: 0.5)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: AppTheme.errorColor.withValues(alpha: 0.05),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.refresh,
|
||||
color: AppTheme.errorColor),
|
||||
title: const Text('Reset Progress',
|
||||
style: TextStyle(color: AppTheme.errorColor)),
|
||||
subtitle:
|
||||
const Text('Resets Level, XP and Training History'),
|
||||
onTap: () => _confirmDangerAction(
|
||||
'Reset Progress?',
|
||||
'This will delete all your workouts and reset your Level to 1. This cannot be undone.',
|
||||
() async {
|
||||
setState(() => _isLoading = true);
|
||||
await userRepo.resetProgress();
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
context.go('/hub');
|
||||
}
|
||||
},
|
||||
),
|
||||
Text(
|
||||
'${_currentBodyweight.toStringAsFixed(1)} kg',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_forever,
|
||||
color: AppTheme.errorColor),
|
||||
title: const Text('Delete Account',
|
||||
style: TextStyle(color: AppTheme.errorColor)),
|
||||
subtitle: const Text(
|
||||
'Permanently delete your account and data'),
|
||||
onTap: () => _confirmDangerAction(
|
||||
'Delete Account?',
|
||||
'Are you sure you want to delete your account? All data will be lost forever.',
|
||||
() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await userRepo.deleteAccount();
|
||||
if (mounted) context.go('/login');
|
||||
} catch (e) {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text('Training Focus',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(color: AppTheme.textPrimary)),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Accessory Template',
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
const SizedBox(height: 12),
|
||||
_buildTemplateSelector(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('Account Security',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(color: AppTheme.textPrimary)),
|
||||
const SizedBox(height: 8),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.lock_outline),
|
||||
title: const Text('Change Password'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: _showChangePasswordDialog,
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(height: 24),
|
||||
Text('Danger Zone',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(color: AppTheme.errorColor)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: AppTheme.errorColor.withValues(alpha: 0.5)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: AppTheme.errorColor.withValues(alpha: 0.05),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.refresh,
|
||||
color: AppTheme.errorColor),
|
||||
title: const Text('Reset Progress',
|
||||
style: TextStyle(color: AppTheme.errorColor)),
|
||||
subtitle: const Text(
|
||||
'Resets Level, XP and Training History'),
|
||||
onTap: () => _confirmDangerAction(
|
||||
'Reset Progress?',
|
||||
'This will delete all your workouts and reset your Level to 1. This cannot be undone.',
|
||||
() async {
|
||||
setState(() => _isLoading = true);
|
||||
await userRepo.resetProgress();
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e')),
|
||||
);
|
||||
context.go('/hub');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_forever,
|
||||
color: AppTheme.errorColor),
|
||||
title: const Text('Delete Account',
|
||||
style: TextStyle(color: AppTheme.errorColor)),
|
||||
subtitle: const Text(
|
||||
'Permanently delete your account and data'),
|
||||
onTap: () => _confirmDangerAction(
|
||||
'Delete Account?',
|
||||
'Are you sure you want to delete your account? All data will be lost forever.',
|
||||
() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await userRepo.deleteAccount();
|
||||
if (mounted) context.go('/login');
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
await userRepo.logout();
|
||||
if (mounted) context.go('/login');
|
||||
},
|
||||
icon: const Icon(Icons.logout),
|
||||
label: const Text('LOGOUT'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
const SizedBox(height: 32),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
await userRepo.logout();
|
||||
if (mounted) context.go('/login');
|
||||
},
|
||||
icon: const Icon(Icons.logout),
|
||||
label: const Text('LOGOUT'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(
|
||||
height: 50,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
Map<String, double> _calculated1RMs = {};
|
||||
Map<String, double> _calculatedTMs = {};
|
||||
|
||||
bool _isAssistedPull = false;
|
||||
bool _isAssistedDip = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -52,16 +55,27 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
void _calculateAll() {
|
||||
final bodyweight = ref.read(onboardingDataProvider)['bodyweight'] ?? 80.0;
|
||||
|
||||
// Squat bleibt gleich...
|
||||
final squatWeight = double.tryParse(_squatWeightController.text) ?? 0;
|
||||
final squatReps = int.tryParse(_squatRepsController.text) ?? 1;
|
||||
final squat1RM = WendlerCalculator.calculate1RM(squatWeight, squatReps);
|
||||
final squatTM = WendlerCalculator.calculateTrainingMax(squat1RM);
|
||||
|
||||
// PULL CALCULATION (Angepasst)
|
||||
double pull1RM = 0.0;
|
||||
if (_canDoPullup) {
|
||||
final added = double.tryParse(_pullWeightController.text) ?? 0;
|
||||
final inputWeight = double.tryParse(_pullWeightController.text) ?? 0;
|
||||
final reps = int.tryParse(_pullRepsController.text) ?? 1;
|
||||
pull1RM = WendlerCalculator.calculate1RM(bodyweight + added, reps);
|
||||
|
||||
// LOGIK: Assisted vs Weighted
|
||||
double totalLoad;
|
||||
if (_isAssistedPull) {
|
||||
totalLoad = (bodyweight - inputWeight).clamp(0.0, double.infinity);
|
||||
} else {
|
||||
totalLoad = bodyweight + inputWeight;
|
||||
}
|
||||
|
||||
pull1RM = WendlerCalculator.calculate1RM(totalLoad, reps);
|
||||
} else {
|
||||
final weight = double.tryParse(_pullWeightController.text) ?? 0;
|
||||
final reps = int.tryParse(_pullRepsController.text) ?? 1;
|
||||
|
|
@ -69,11 +83,21 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
}
|
||||
final pullTM = WendlerCalculator.calculateTrainingMax(pull1RM);
|
||||
|
||||
// PUSH CALCULATION (Angepasst)
|
||||
double push1RM = 0.0;
|
||||
if (_canDoDip) {
|
||||
final added = double.tryParse(_dipWeightController.text) ?? 0;
|
||||
final inputWeight = double.tryParse(_dipWeightController.text) ?? 0;
|
||||
final reps = int.tryParse(_pushRepsController.text) ?? 1;
|
||||
push1RM = WendlerCalculator.calculate1RM(bodyweight + added, reps);
|
||||
|
||||
// LOGIK: Assisted vs Weighted
|
||||
double totalLoad;
|
||||
if (_isAssistedDip) {
|
||||
totalLoad = (bodyweight - inputWeight).clamp(0.0, double.infinity);
|
||||
} else {
|
||||
totalLoad = bodyweight + inputWeight;
|
||||
}
|
||||
|
||||
push1RM = WendlerCalculator.calculate1RM(totalLoad, reps);
|
||||
} else {
|
||||
final weight = double.tryParse(_benchWeightController.text) ?? 0;
|
||||
final reps = int.tryParse(_pushRepsController.text) ?? 1;
|
||||
|
|
@ -95,6 +119,52 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
// void _calculateAll() {
|
||||
// final bodyweight = ref.read(onboardingDataProvider)['bodyweight'] ?? 80.0;
|
||||
|
||||
// final squatWeight = double.tryParse(_squatWeightController.text) ?? 0;
|
||||
// final squatReps = int.tryParse(_squatRepsController.text) ?? 1;
|
||||
// final squat1RM = WendlerCalculator.calculate1RM(squatWeight, squatReps);
|
||||
// final squatTM = WendlerCalculator.calculateTrainingMax(squat1RM);
|
||||
|
||||
// double pull1RM = 0.0;
|
||||
// if (_canDoPullup) {
|
||||
// final added = double.tryParse(_pullWeightController.text) ?? 0;
|
||||
// final reps = int.tryParse(_pullRepsController.text) ?? 1;
|
||||
// pull1RM = WendlerCalculator.calculate1RM(bodyweight + added, reps);
|
||||
// } else {
|
||||
// final weight = double.tryParse(_pullWeightController.text) ?? 0;
|
||||
// final reps = int.tryParse(_pullRepsController.text) ?? 1;
|
||||
// pull1RM = WendlerCalculator.calculate1RM(weight, reps);
|
||||
// }
|
||||
// final pullTM = WendlerCalculator.calculateTrainingMax(pull1RM);
|
||||
|
||||
// double push1RM = 0.0;
|
||||
// if (_canDoDip) {
|
||||
// final added = double.tryParse(_dipWeightController.text) ?? 0;
|
||||
// final reps = int.tryParse(_pushRepsController.text) ?? 1;
|
||||
// push1RM = WendlerCalculator.calculate1RM(bodyweight + added, reps);
|
||||
// } else {
|
||||
// final weight = double.tryParse(_benchWeightController.text) ?? 0;
|
||||
// final reps = int.tryParse(_pushRepsController.text) ?? 1;
|
||||
// push1RM = WendlerCalculator.calculate1RM(weight, reps);
|
||||
// }
|
||||
// final pushTM = WendlerCalculator.calculateTrainingMax(push1RM);
|
||||
|
||||
// setState(() {
|
||||
// _calculated1RMs = {
|
||||
// 'squat': squat1RM,
|
||||
// 'pullup': pull1RM,
|
||||
// 'dip': push1RM,
|
||||
// };
|
||||
// _calculatedTMs = {
|
||||
// 'squat': squatTM,
|
||||
// 'pullup': pullTM,
|
||||
// 'dip': pushTM,
|
||||
// };
|
||||
// });
|
||||
// }
|
||||
|
||||
void _handleContinue() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
|
|
@ -171,12 +241,24 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
_calculateAll();
|
||||
});
|
||||
},
|
||||
isAssisted: _isAssistedPull,
|
||||
onToggleAssisted: (val) {
|
||||
setState(() {
|
||||
_isAssistedPull = val;
|
||||
_calculateAll();
|
||||
});
|
||||
},
|
||||
weightController: _pullWeightController,
|
||||
repsController: _pullRepsController,
|
||||
weightLabel:
|
||||
_canDoPullup ? 'Add. Weight (kg)' : 'Row Weight (kg)',
|
||||
weightLabel: _canDoPullup
|
||||
? (_isAssistedPull
|
||||
? 'Band Assistance (kg)'
|
||||
: 'Added Weight (kg)')
|
||||
: 'Row Weight (kg)',
|
||||
// weightLabel:
|
||||
// _canDoPullup ? 'Add. Weight (kg)' : 'Row Weight (kg)',
|
||||
repsLabel: _canDoPullup ? 'Reps' : '5RM Reps (usually 5)',
|
||||
showResults: _canDoPullup || true,
|
||||
showResults: true,
|
||||
result1RM: _calculated1RMs['pullup'] ?? 0,
|
||||
resultTM: _calculatedTMs['pullup'] ?? 0,
|
||||
onChanged: _calculateAll,
|
||||
|
|
@ -196,10 +278,22 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
_calculateAll();
|
||||
});
|
||||
},
|
||||
isAssisted: _isAssistedDip,
|
||||
onToggleAssisted: (val) {
|
||||
setState(() {
|
||||
_isAssistedDip = val;
|
||||
_calculateAll();
|
||||
});
|
||||
},
|
||||
weightController:
|
||||
_canDoDip ? _dipWeightController : _benchWeightController,
|
||||
repsController: _pushRepsController,
|
||||
weightLabel: _canDoDip ? 'Add. Weight (kg)' : 'Weight (kg)',
|
||||
weightLabel: _canDoDip
|
||||
? (_isAssistedDip
|
||||
? 'Band Assistance (kg)'
|
||||
: 'Added Weight (kg)')
|
||||
: 'Weight (kg)',
|
||||
// weightLabel: _canDoDip ? 'Add. Weight (kg)' : 'Weight (kg)',
|
||||
repsLabel: 'Reps',
|
||||
showWeightInput: true,
|
||||
showResults: true,
|
||||
|
|
@ -295,7 +389,10 @@ class _ExerciseCard extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(exerciseName,
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(color: AppTheme.textPrimary)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -357,6 +454,8 @@ class _AdaptiveExerciseCard extends StatelessWidget {
|
|||
final double result1RM;
|
||||
final double resultTM;
|
||||
final VoidCallback onChanged;
|
||||
final bool isAssisted;
|
||||
final ValueChanged<bool>? onToggleAssisted;
|
||||
|
||||
const _AdaptiveExerciseCard({
|
||||
required this.slotTitle,
|
||||
|
|
@ -374,6 +473,8 @@ class _AdaptiveExerciseCard extends StatelessWidget {
|
|||
required this.result1RM,
|
||||
required this.resultTM,
|
||||
required this.onChanged,
|
||||
this.isAssisted = false,
|
||||
this.onToggleAssisted,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -384,14 +485,15 @@ class _AdaptiveExerciseCard extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text(slotTitle.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold)),
|
||||
]),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(slotTitle.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold)),
|
||||
Row(
|
||||
children: [
|
||||
Text('Can do 1 rep?',
|
||||
|
|
@ -407,6 +509,24 @@ class _AdaptiveExerciseCard extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
if (isCapable && onToggleAssisted != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text('Assisted (Bands)?',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isAssisted
|
||||
? AppTheme.primaryColor
|
||||
: Colors.grey)),
|
||||
Switch(
|
||||
value: isAssisted,
|
||||
activeThumbColor: AppTheme.primaryColor,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onChanged: onToggleAssisted,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
|
@ -421,13 +541,16 @@ class _AdaptiveExerciseCard extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(isCapable ? primaryName : secondaryName,
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(color: AppTheme.textPrimary)),
|
||||
],
|
||||
),
|
||||
if (!isCapable) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Adjusted Strategy: ${isCapable ? "Wendler 5/3/1" : "Linear Progression (3x5)"}',
|
||||
'Adjusted: ${"Wendler 5/3/1"}',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.secondaryColor,
|
||||
fontSize: 12,
|
||||
|
|
|
|||
|
|
@ -352,10 +352,14 @@ class _CurrentCycleCard extends StatelessWidget {
|
|||
Text('Current Training Maxes (TM)',
|
||||
style: Theme.of(context).textTheme.labelLarge),
|
||||
const SizedBox(height: 16),
|
||||
_StatRow(label: 'Squat', value: '${tms['squat']} kg'),
|
||||
_StatRow(
|
||||
label: getLabel(pullVariant), value: '${tms['pullup']} kg'),
|
||||
_StatRow(label: getLabel(pushVariant), value: '${tms['dip']} kg'),
|
||||
label: 'Squat', value: '${tms['squat'].toStringAsFixed(2)} kg'),
|
||||
_StatRow(
|
||||
label: getLabel(pullVariant),
|
||||
value: '${tms['pullup'].toStringAsFixed(2)} kg'),
|
||||
_StatRow(
|
||||
label: getLabel(pushVariant),
|
||||
value: '${tms['dip'].toStringAsFixed(2)} kg'),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
|
|
@ -468,15 +472,15 @@ class _DiffRow extends StatelessWidget {
|
|||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(name)),
|
||||
Text('${oldVal.toStringAsFixed(1)} → ',
|
||||
Text('${oldVal.toStringAsFixed(2)} → ',
|
||||
style: const TextStyle(color: Colors.grey)),
|
||||
Text(
|
||||
newVal.toStringAsFixed(1),
|
||||
newVal.toStringAsFixed(2),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (isPositive)
|
||||
Text('+${diff.toStringAsFixed(1)}',
|
||||
Text('+${diff.toStringAsFixed(2)}',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.successColor, fontWeight: FontWeight.bold))
|
||||
else
|
||||
|
|
|
|||
|
|
@ -67,22 +67,14 @@ class WorkoutGeneratorService {
|
|||
List<WorkoutSet> sets;
|
||||
|
||||
if (isMain) {
|
||||
if (type == ExerciseType.row || type == ExerciseType.bench) {
|
||||
sets = WendlerCalculator.generateLinearSets(
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: user.currentBodyweight);
|
||||
} else {
|
||||
sets = WendlerCalculator.generateSets(
|
||||
week: week,
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: user.currentBodyweight,
|
||||
);
|
||||
}
|
||||
sets = WendlerCalculator.generateSets(
|
||||
week: week,
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: user.currentBodyweight,
|
||||
);
|
||||
} else {
|
||||
if (week == 4) return;
|
||||
if (type == ExerciseType.row || type == ExerciseType.bench) return;
|
||||
|
||||
sets = WendlerCalculator.generateFSLSets(
|
||||
trainingMax: tm,
|
||||
|
|
@ -156,8 +148,8 @@ class WorkoutGeneratorService {
|
|||
weight: calculateWeight(squatTm, 0.4)));
|
||||
|
||||
accessories.add(_createIntervalExercise(
|
||||
id: 'kb_swing',
|
||||
name: '2H KB Swing',
|
||||
id: 'kb_snatch_acc',
|
||||
name: 'KB Snatch',
|
||||
sets: 10,
|
||||
intervalSeconds: 60,
|
||||
repsPerSet: 10));
|
||||
|
|
@ -178,8 +170,8 @@ class WorkoutGeneratorService {
|
|||
weight: calculateWeight(pullupTm, 0.2)));
|
||||
|
||||
accessories.add(_createIntervalExercise(
|
||||
id: 'kb_snatch_acc',
|
||||
name: 'KB Snatch',
|
||||
id: 'kb_swing',
|
||||
name: '2H KB Swing',
|
||||
sets: 10,
|
||||
intervalSeconds: 60,
|
||||
repsPerSet: 5));
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import '../widgets/plate_visualizer.dart';
|
|||
import '../widgets/enemy_hp_bar.dart';
|
||||
import '../../../gamification/application/quest_service.dart';
|
||||
import '../widgets/emom_timer_widget.dart';
|
||||
import '../widgets/timer_widget.dart';
|
||||
|
||||
class BattleScreen extends ConsumerStatefulWidget {
|
||||
final int week;
|
||||
|
|
@ -615,7 +616,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
Positioned.fill(
|
||||
child: SafeArea(
|
||||
child: _isResting
|
||||
? _buildRestScreen()
|
||||
? _buildRestScreen(inventory)
|
||||
: _buildWorkoutScreen(currentExercise, currentSet,
|
||||
plateResult, completedHP, totalHP),
|
||||
),
|
||||
|
|
@ -626,7 +627,20 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
Widget _buildRestScreen() {
|
||||
Widget _buildRestScreen(Map<String, dynamic> inventory) {
|
||||
WorkoutSet? nextSet;
|
||||
Exercise? nextExerciseInfo;
|
||||
|
||||
if (_currentSetIndex + 1 < _exercises[_currentExerciseIndex].sets.length) {
|
||||
nextExerciseInfo = _exercises[_currentExerciseIndex];
|
||||
nextSet = nextExerciseInfo.sets[_currentSetIndex + 1];
|
||||
} else if (_currentExerciseIndex + 1 < _exercises.length) {
|
||||
nextExerciseInfo = _exercises[_currentExerciseIndex + 1];
|
||||
if (nextExerciseInfo.sets.isNotEmpty) {
|
||||
nextSet = nextExerciseInfo.sets.first;
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
|
|
@ -639,51 +653,151 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'REST',
|
||||
style: Theme.of(context).textTheme.displayLarge,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
height: 200,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
height: 200,
|
||||
child: CircularProgressIndicator(
|
||||
value: _restSeconds / 180,
|
||||
strokeWidth: 12,
|
||||
backgroundColor: AppTheme.xpBarBackground,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatTime(_restSeconds),
|
||||
style: Theme.of(context).textTheme.displayLarge?.copyWith(
|
||||
fontSize: 48,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'REST',
|
||||
style: Theme.of(context).textTheme.displayLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
ElevatedButton(
|
||||
onPressed: _skipRest,
|
||||
child: const Text('SKIP REST'),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
height: 200,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
height: 200,
|
||||
child: CircularProgressIndicator(
|
||||
value: _restSeconds / 180,
|
||||
strokeWidth: 12,
|
||||
backgroundColor: AppTheme.xpBarBackground,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatTime(_restSeconds),
|
||||
style: Theme.of(context).textTheme.displayLarge?.copyWith(
|
||||
fontSize: 32,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: _skipRest,
|
||||
child: const Text('SKIP REST'),
|
||||
),
|
||||
if (nextSet != null && nextExerciseInfo != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
const Divider(color: Colors.white10, endIndent: 32, indent: 32),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'UP NEXT: ${nextExerciseInfo.exerciseName.toUpperCase()}',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${nextSet.repsTarget} x ${nextSet.targetWeightTotal > 0 ? "${nextSet.targetWeightTotal} kg" : "Bodyweight"}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (nextSet.targetWeightTotal > 0)
|
||||
_buildNextSetPlates(nextExerciseInfo, nextSet, inventory),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNextSetPlates(
|
||||
Exercise exercise, WorkoutSet set, Map<String, dynamic> inventory) {
|
||||
final isTwoSided = exercise.exerciseId == 'squat' ||
|
||||
exercise.exerciseId == 'row' ||
|
||||
exercise.exerciseId == 'bench' ||
|
||||
exercise.exerciseId == 'rdl' ||
|
||||
exercise.exerciseId == 'ohp' ||
|
||||
exercise.exerciseId == 'curl';
|
||||
|
||||
if (!isTwoSided) return const SizedBox.shrink();
|
||||
|
||||
final barWeight = (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0;
|
||||
final platesList = (inventory['plates'] as List?)
|
||||
?.map((e) => (e as num).toDouble())
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
final plateResult = PlateCalculator.calculate(
|
||||
targetWeight: set.targetWeightTotal,
|
||||
barWeight: barWeight,
|
||||
availablePlates: platesList,
|
||||
availableBands: {},
|
||||
isTwoSided: true,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: PlateVisualizer(
|
||||
plateConfiguration: plateResult.plateConfiguration,
|
||||
isTwoSided: true,
|
||||
exerciseName: '',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget _buildNextSetPlates(
|
||||
// Exercise exercise, WorkoutSet set, Map<String, dynamic> inventory) {
|
||||
// final isTwoSided = exercise.exerciseId == 'squat' ||
|
||||
// exercise.exerciseId == 'row' ||
|
||||
// exercise.exerciseId == 'bench' ||
|
||||
// exercise.exerciseId == 'rdl' ||
|
||||
// exercise.exerciseId == 'ohp' ||
|
||||
// exercise.exerciseId == 'curl';
|
||||
|
||||
// if (!isTwoSided) return const SizedBox.shrink();
|
||||
|
||||
// final barWeight = (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0;
|
||||
// final platesList = (inventory['plates'] as List?)
|
||||
// ?.map((e) => (e as num).toDouble())
|
||||
// .toList() ??
|
||||
// [];
|
||||
|
||||
// final plateResult = PlateCalculator.calculate(
|
||||
// targetWeight: set.targetWeightTotal,
|
||||
// barWeight: barWeight,
|
||||
// availablePlates: platesList,
|
||||
// availableBands: {},
|
||||
// isTwoSided: true,
|
||||
// );
|
||||
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.only(top: 12.0),
|
||||
// child: SizedBox(
|
||||
// height: 50,
|
||||
// child: PlateVisualizer(
|
||||
// plateConfiguration: plateResult.plateConfiguration,
|
||||
// isTwoSided: true,
|
||||
// exerciseName: '',
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
Widget _buildWorkoutScreen(
|
||||
Exercise currentExercise,
|
||||
WorkoutSet currentSet,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,24 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
|
|||
_secondsRemaining = widget.intervalSeconds;
|
||||
_audioPlayer = AudioPlayer();
|
||||
|
||||
_audioPlayer.setAudioContext(
|
||||
AudioContext(
|
||||
android: AudioContextAndroid(
|
||||
isSpeakerphoneOn: false,
|
||||
stayAwake: false,
|
||||
contentType: AndroidContentType.sonification,
|
||||
usageType: AndroidUsageType.notificationEvent,
|
||||
audioFocus: AndroidAudioFocus.none,
|
||||
),
|
||||
// iOS: AudioContextIOS(
|
||||
// category: AVAudioSessionCategory.ambient,
|
||||
// options: [
|
||||
// AVAudioSessionOptions.mixWithOthers,
|
||||
// ],
|
||||
// ),
|
||||
),
|
||||
);
|
||||
|
||||
_pulseController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ class PlateVisualizer extends StatelessWidget {
|
|||
if (isTwoSided) _buildBarbellView() else _buildBeltView(),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Total: ${plateConfiguration.fold<double>(0, (sum, p) => sum + p).toStringAsFixed(1)} kg ${isTwoSided ? 'per side' : ''}',
|
||||
'Total: ${plateConfiguration.fold<double>(0, (sum, p) => sum + p).toStringAsFixed(2)} kg ${isTwoSided ? 'per side' : ''}',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -62,7 +62,9 @@ class WendlerCalculator {
|
|||
final rounded = _roundWeight(targetTotal, exerciseType);
|
||||
|
||||
double plateWeight = 0;
|
||||
if (exerciseType != ExerciseType.squat) {
|
||||
if (exerciseType != ExerciseType.squat ||
|
||||
exerciseType != ExerciseType.row ||
|
||||
exerciseType != ExerciseType.bench) {
|
||||
plateWeight = max(0, rounded - currentBodyweight);
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +146,9 @@ class WendlerCalculator {
|
|||
final rounded = _roundWeight(targetTotal, exerciseType);
|
||||
|
||||
double plateWeight = 0;
|
||||
if (exerciseType != ExerciseType.squat) {
|
||||
if (exerciseType != ExerciseType.squat ||
|
||||
exerciseType != ExerciseType.row ||
|
||||
exerciseType != ExerciseType.bench) {
|
||||
plateWeight = max(0, rounded - currentBodyweight);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue