slrpg-app/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart

648 lines
23 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:slrpg_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../shared/domain/logic/wendler_calculator.dart';
import 'bodyweight_input_screen.dart';
class StrengthTestScreen extends ConsumerStatefulWidget {
const StrengthTestScreen({super.key});
@override
ConsumerState<StrengthTestScreen> createState() => _StrengthTestScreenState();
}
class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
final _formKey = GlobalKey<FormState>();
final _squatWeightController = TextEditingController(text: '60');
final _squatRepsController = TextEditingController(text: '5');
bool _canDoPullup = true;
final _pullWeightController = TextEditingController(text: '0');
final _pullRepsController = TextEditingController(text: '5');
bool _canDoDip = true;
final _dipWeightController = TextEditingController(text: '0');
final _benchWeightController = TextEditingController(text: '40');
final _pushRepsController = TextEditingController(text: '5');
Map<String, double> _calculated1RMs = {};
Map<String, double> _calculatedTMs = {};
bool _isAssistedPull = false;
bool _isAssistedDip = false;
@override
void initState() {
super.initState();
_calculateAll();
}
@override
void dispose() {
_squatWeightController.dispose();
_squatRepsController.dispose();
_pullWeightController.dispose();
_pullRepsController.dispose();
_dipWeightController.dispose();
_pushRepsController.dispose();
super.dispose();
}
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 inputWeight = double.tryParse(_pullWeightController.text) ?? 0;
final reps = int.tryParse(_pullRepsController.text) ?? 1;
// 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;
pull1RM = WendlerCalculator.calculate1RM(weight, reps);
}
final pullTM = WendlerCalculator.calculateTrainingMax(pull1RM);
// PUSH CALCULATION (Angepasst)
double push1RM = 0.0;
if (_canDoDip) {
final inputWeight = double.tryParse(_dipWeightController.text) ?? 0;
final reps = int.tryParse(_pushRepsController.text) ?? 1;
// 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;
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 _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;
final variants = <String, String>{
'pull': _canDoPullup ? 'pullup' : 'row',
'push': _canDoDip ? 'dip' : 'bench',
};
ref.read(onboardingDataProvider.notifier).updateData({
'training_maxes': _calculatedTMs,
'exercise_variants': variants,
});
context.go('/onboarding/inventory');
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.strengthTestTitle),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/onboarding/bodyweight'),
),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LinearProgressIndicator(
value: 0.5,
backgroundColor: AppTheme.xpBarBackground,
color: AppTheme.primaryColor,
),
const SizedBox(height: 32),
Text(
l10n.strengthTestSubtitle,
style: Theme.of(context).textTheme.displayMedium,
),
const SizedBox(height: 8),
Text(
l10n.strengthTestBody,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
_ExerciseCard(
title: l10n.strengthLegs,
exerciseName: 'Back Squat',
icon: Icons.accessibility_new,
weightController: _squatWeightController,
repsController: _squatRepsController,
isBodyweight: false,
onChanged: _calculateAll,
result1RM: _calculated1RMs['squat'] ?? 0,
resultTM: _calculatedTMs['squat'] ?? 0,
),
const SizedBox(height: 16),
_AdaptiveExerciseCard(
slotTitle: l10n.strengthPull,
primaryName: 'Weighted Pull-up',
secondaryName: 'Pendlay Row',
icon: Icons.north,
isCapable: _canDoPullup,
onToggleCapability: (val) {
setState(() {
_canDoPullup = val;
_pullWeightController.text = '0';
_pullRepsController.text = '5';
_calculateAll();
});
},
isAssisted: _isAssistedPull,
onToggleAssisted: (val) {
setState(() {
_isAssistedPull = val;
_calculateAll();
});
},
weightController: _pullWeightController,
repsController: _pullRepsController,
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: true,
result1RM: _calculated1RMs['pullup'] ?? 0,
resultTM: _calculatedTMs['pullup'] ?? 0,
onChanged: _calculateAll,
),
const SizedBox(height: 16),
_AdaptiveExerciseCard(
slotTitle: l10n.strengthPush,
primaryName: 'Weighted Dip',
secondaryName: 'Bench Press',
icon: Icons.south,
isCapable: _canDoDip,
onToggleCapability: (val) {
setState(() {
_canDoDip = val;
_dipWeightController.text = '0';
_pushRepsController.text = '5';
_calculateAll();
});
},
isAssisted: _isAssistedDip,
onToggleAssisted: (val) {
setState(() {
_isAssistedDip = val;
_calculateAll();
});
},
weightController:
_canDoDip ? _dipWeightController : _benchWeightController,
repsController: _pushRepsController,
weightLabel: _canDoDip
? (_isAssistedDip
? 'Band Assistance (kg)'
: 'Added Weight (kg)')
: 'Weight (kg)',
// weightLabel: _canDoDip ? 'Add. Weight (kg)' : 'Weight (kg)',
repsLabel: 'Reps',
showWeightInput: true,
showResults: true,
result1RM: _calculated1RMs['dip'] ?? 0,
resultTM: _calculatedTMs['dip'] ?? 0,
onChanged: _calculateAll,
),
const SizedBox(height: 32),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.primaryColor.withValues(alpha: 0.3)),
),
child: Row(
children: [
const Icon(Icons.info_outline,
color: AppTheme.primaryColor),
const SizedBox(width: 12),
Expanded(
child: Text(
l10n.tmExplanation,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: AppTheme.textSecondary),
),
),
],
),
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _handleContinue,
child: Text(l10n.continueButton),
),
],
),
),
),
),
);
}
}
class _ExerciseCard extends StatelessWidget {
final String title;
final String exerciseName;
final IconData icon;
final TextEditingController weightController;
final TextEditingController repsController;
final bool isBodyweight;
final double result1RM;
final double resultTM;
final VoidCallback onChanged;
const _ExerciseCard({
required this.title,
required this.exerciseName,
required this.icon,
required this.weightController,
required this.repsController,
required this.isBodyweight,
required this.result1RM,
required this.resultTM,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title.toUpperCase(),
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8)),
child: Icon(icon, color: AppTheme.primaryColor),
),
const SizedBox(width: 12),
Text(exerciseName,
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(color: AppTheme.textPrimary)),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: weightController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d+\.?\d{0,2}'))
],
decoration: InputDecoration(
labelText: isBodyweight
? l10n.addWeightLabel
: l10n.weightLabel,
isDense: true),
onChanged: (_) => onChanged(),
validator: (v) => v!.isEmpty ? 'Required' : null,
),
),
const SizedBox(width: 12),
Expanded(
child: TextFormField(
controller: repsController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
labelText: l10n.repsLabel, isDense: true),
onChanged: (_) => onChanged(),
validator: (v) => v!.isEmpty ? 'Required' : null,
),
),
],
),
const SizedBox(height: 16),
_ResultBox(rm: result1RM, tm: resultTM),
],
),
),
);
}
}
class _AdaptiveExerciseCard extends StatelessWidget {
final String slotTitle;
final String primaryName;
final String secondaryName;
final IconData icon;
final bool isCapable;
final ValueChanged<bool> onToggleCapability;
final TextEditingController weightController;
final TextEditingController repsController;
final String weightLabel;
final String repsLabel;
final bool showWeightInput;
final bool showResults;
final double result1RM;
final double resultTM;
final VoidCallback onChanged;
final bool isAssisted;
final ValueChanged<bool>? onToggleAssisted;
const _AdaptiveExerciseCard({
required this.slotTitle,
required this.primaryName,
required this.secondaryName,
required this.icon,
required this.isCapable,
required this.onToggleCapability,
required this.weightController,
required this.repsController,
required this.weightLabel,
required this.repsLabel,
this.showWeightInput = true,
this.showResults = true,
required this.result1RM,
required this.resultTM,
required this.onChanged,
this.isAssisted = false,
this.onToggleAssisted,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
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(
children: [
Row(
children: [
Text(l10n.canDoOneRep,
style: TextStyle(
fontSize: 12,
color: isCapable
? AppTheme.successColor
: Colors.grey)),
Switch(
value: isCapable,
activeThumbColor: AppTheme.successColor,
onChanged: onToggleCapability,
),
],
),
if (isCapable && onToggleAssisted != null)
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(l10n.isAssisted,
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),
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8)),
child: Icon(icon, color: AppTheme.primaryColor),
),
const SizedBox(width: 12),
Text(isCapable ? primaryName : secondaryName,
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(color: AppTheme.textPrimary)),
],
),
if (!isCapable) ...[
const SizedBox(height: 8),
Text(
'Adjusted: ${"Wendler 5/3/1"}',
style: const TextStyle(
color: AppTheme.secondaryColor,
fontSize: 12,
fontStyle: FontStyle.italic),
),
],
const SizedBox(height: 16),
Row(
children: [
if (showWeightInput)
Expanded(
flex: 2,
child: TextFormField(
controller: weightController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d+\.?\d{0,2}'))
],
decoration: InputDecoration(
labelText: weightLabel, isDense: true),
onChanged: (_) => onChanged(),
validator: (v) => v!.isEmpty ? 'Required' : null,
),
)
else
const Spacer(flex: 2),
if (showWeightInput) const SizedBox(width: 12),
Expanded(
child: TextFormField(
controller: repsController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration:
InputDecoration(labelText: repsLabel, isDense: true),
onChanged: (_) => onChanged(),
validator: (v) => v!.isEmpty ? 'Required' : null,
),
),
],
),
if (showResults) ...[
const SizedBox(height: 16),
_ResultBox(rm: result1RM, tm: resultTM),
],
],
),
),
);
}
}
class _ResultBox extends StatelessWidget {
final double rm;
final double tm;
const _ResultBox({required this.rm, required this.tm});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.surfaceColor, borderRadius: BorderRadius.circular(8)),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(l10n.est1rm),
Text('${rm.toStringAsFixed(1)} kg',
style: Theme.of(context).textTheme.bodyLarge),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(l10n.trainingMaxLabel),
Text('${tm.toStringAsFixed(1)} kg',
style: const TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold)),
],
),
],
),
);
}
}