648 lines
23 KiB
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)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|