feat: add assisted exercise an next set preview

This commit is contained in:
Patryk Hegenberg 2026-01-06 10:29:43 +01:00
parent 44f5703de4
commit 79a7e1c50d
10 changed files with 534 additions and 270 deletions

4
.gitignore vendored
View file

@ -43,3 +43,7 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
.env
.env.production
.env.development

View file

@ -10,11 +10,11 @@ void main() async {
try { try {
await dotenv.load(fileName: '.env'); await dotenv.load(fileName: '.env');
debugPrint('Environment loaded: ${dotenv.env['ENVIRONMENT']}'); debugPrint('Environment loaded: ${dotenv.env['ENVIRONMENT']}');
debugPrint('API URL: ${dotenv.env['API_BASE_URL']}'); debugPrint('API URL: ${dotenv.env['API_BASE_URL']}');
} catch (e) { } catch (e) {
debugPrint('⚠️ Could not load .env file: $e'); debugPrint('Could not load .env file: $e');
debugPrint('⚠️ Using default production values'); debugPrint('Using default production values');
} }
await SystemChrome.setPreferredOrientations([ await SystemChrome.setPreferredOrientations([

View file

@ -425,199 +425,204 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
), ),
body: _isLoading body: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: ListView( : SafeArea(
padding: const EdgeInsets.all(16), child: ListView(
children: [ padding: const EdgeInsets.all(16),
Center( children: [
child: Stack( Center(
children: [ child: Stack(
AvatarRenderer( children: [
config: avatarConfig, AvatarRenderer(
size: 120, config: avatarConfig,
), size: 120,
Positioned( ),
bottom: 0, Positioned(
right: 0, bottom: 0,
child: CircleAvatar( right: 0,
backgroundColor: AppTheme.surfaceColor, child: CircleAvatar(
radius: 18, backgroundColor: AppTheme.surfaceColor,
child: IconButton( radius: 18,
icon: const Icon(Icons.edit, size: 16), child: IconButton(
onPressed: _showAvatarEditor, icon: const Icon(Icons.edit, size: 16),
onPressed: _showAvatarEditor,
),
), ),
), ),
), ],
], ),
), ),
), const SizedBox(height: 32),
const SizedBox(height: 32), Center(
Center( child: OutlinedButton.icon(
child: OutlinedButton.icon( onPressed: _showBackgroundSelector,
onPressed: _showBackgroundSelector, icon: const Icon(Icons.landscape),
icon: const Icon(Icons.landscape), label: const Text('CHANGE SCENERY'),
label: const Text('CHANGE SCENERY'), ),
), ),
), const SizedBox(height: 32),
const SizedBox(height: 32), Text('Physical Stats',
Text('Physical Stats', style: Theme.of(context)
style: Theme.of(context) .textTheme
.textTheme .titleLarge
.titleLarge ?.copyWith(color: AppTheme.textPrimary)),
?.copyWith(color: AppTheme.textPrimary)), const SizedBox(height: 16),
const SizedBox(height: 16), Card(
Card( child: Padding(
child: Padding( padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text('Current Bodyweight',
Text('Current Bodyweight', style: Theme.of(context).textTheme.bodyMedium),
style: Theme.of(context).textTheme.bodyMedium), Row(
Row( children: [
children: [ Expanded(
Expanded( child: Slider(
child: Slider( value: _currentBodyweight,
value: _currentBodyweight, min: 40,
min: 40, max: 150,
max: 150, divisions: 220,
divisions: 220, label: _currentBodyweight.toStringAsFixed(1),
label: _currentBodyweight.toStringAsFixed(1), activeColor: AppTheme.primaryColor,
activeColor: AppTheme.primaryColor, onChanged: (val) => setState(() {
onChanged: (val) => setState(() { _currentBodyweight = val;
_currentBodyweight = val; _hasChanges = true;
_hasChanges = true; }),
}), ),
), ),
), Text(
Text( '${_currentBodyweight.toStringAsFixed(1)} kg',
'${_currentBodyweight.toStringAsFixed(1)} kg', style: Theme.of(context)
style: Theme.of(context) .textTheme
.textTheme .titleMedium
.titleMedium ?.copyWith(
?.copyWith( fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, color: AppTheme.primaryColor,
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');
}
},
),
), ),
const Divider(height: 1), ),
ListTile( ),
leading: const Icon(Icons.delete_forever, const SizedBox(height: 32),
color: AppTheme.errorColor), Text('Training Focus',
title: const Text('Delete Account', style: Theme.of(context)
style: TextStyle(color: AppTheme.errorColor)), .textTheme
subtitle: const Text( .titleLarge
'Permanently delete your account and data'), ?.copyWith(color: AppTheme.textPrimary)),
onTap: () => _confirmDangerAction( const SizedBox(height: 16),
'Delete Account?', Card(
'Are you sure you want to delete your account? All data will be lost forever.', child: Padding(
() async { padding: const EdgeInsets.all(16),
setState(() => _isLoading = true); child: Column(
try { crossAxisAlignment: CrossAxisAlignment.start,
await userRepo.deleteAccount(); children: [
if (mounted) context.go('/login'); Text('Accessory Template',
} catch (e) { 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) { if (mounted) {
setState(() => _isLoading = false); setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar( context.go('/hub');
SnackBar(content: Text('Error: $e')),
);
} }
} },
}, ),
), ),
), 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),
const SizedBox(height: 32), OutlinedButton.icon(
OutlinedButton.icon( onPressed: () async {
onPressed: () async { await userRepo.logout();
await userRepo.logout(); if (mounted) context.go('/login');
if (mounted) context.go('/login'); },
}, icon: const Icon(Icons.logout),
icon: const Icon(Icons.logout), label: const Text('LOGOUT'),
label: const Text('LOGOUT'), style: OutlinedButton.styleFrom(
style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16),
padding: const EdgeInsets.symmetric(vertical: 16), ),
), ),
), const SizedBox(
], height: 50,
)
],
),
), ),
); );
} }

View file

@ -32,6 +32,9 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
Map<String, double> _calculated1RMs = {}; Map<String, double> _calculated1RMs = {};
Map<String, double> _calculatedTMs = {}; Map<String, double> _calculatedTMs = {};
bool _isAssistedPull = false;
bool _isAssistedDip = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -52,16 +55,27 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
void _calculateAll() { void _calculateAll() {
final bodyweight = ref.read(onboardingDataProvider)['bodyweight'] ?? 80.0; final bodyweight = ref.read(onboardingDataProvider)['bodyweight'] ?? 80.0;
// Squat bleibt gleich...
final squatWeight = double.tryParse(_squatWeightController.text) ?? 0; final squatWeight = double.tryParse(_squatWeightController.text) ?? 0;
final squatReps = int.tryParse(_squatRepsController.text) ?? 1; final squatReps = int.tryParse(_squatRepsController.text) ?? 1;
final squat1RM = WendlerCalculator.calculate1RM(squatWeight, squatReps); final squat1RM = WendlerCalculator.calculate1RM(squatWeight, squatReps);
final squatTM = WendlerCalculator.calculateTrainingMax(squat1RM); final squatTM = WendlerCalculator.calculateTrainingMax(squat1RM);
// PULL CALCULATION (Angepasst)
double pull1RM = 0.0; double pull1RM = 0.0;
if (_canDoPullup) { if (_canDoPullup) {
final added = double.tryParse(_pullWeightController.text) ?? 0; final inputWeight = double.tryParse(_pullWeightController.text) ?? 0;
final reps = int.tryParse(_pullRepsController.text) ?? 1; 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 { } else {
final weight = double.tryParse(_pullWeightController.text) ?? 0; final weight = double.tryParse(_pullWeightController.text) ?? 0;
final reps = int.tryParse(_pullRepsController.text) ?? 1; final reps = int.tryParse(_pullRepsController.text) ?? 1;
@ -69,11 +83,21 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
} }
final pullTM = WendlerCalculator.calculateTrainingMax(pull1RM); final pullTM = WendlerCalculator.calculateTrainingMax(pull1RM);
// PUSH CALCULATION (Angepasst)
double push1RM = 0.0; double push1RM = 0.0;
if (_canDoDip) { if (_canDoDip) {
final added = double.tryParse(_dipWeightController.text) ?? 0; final inputWeight = double.tryParse(_dipWeightController.text) ?? 0;
final reps = int.tryParse(_pushRepsController.text) ?? 1; 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 { } else {
final weight = double.tryParse(_benchWeightController.text) ?? 0; final weight = double.tryParse(_benchWeightController.text) ?? 0;
final reps = int.tryParse(_pushRepsController.text) ?? 1; 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() { void _handleContinue() {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
@ -171,12 +241,24 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
_calculateAll(); _calculateAll();
}); });
}, },
isAssisted: _isAssistedPull,
onToggleAssisted: (val) {
setState(() {
_isAssistedPull = val;
_calculateAll();
});
},
weightController: _pullWeightController, weightController: _pullWeightController,
repsController: _pullRepsController, repsController: _pullRepsController,
weightLabel: weightLabel: _canDoPullup
_canDoPullup ? 'Add. Weight (kg)' : 'Row Weight (kg)', ? (_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)', repsLabel: _canDoPullup ? 'Reps' : '5RM Reps (usually 5)',
showResults: _canDoPullup || true, showResults: true,
result1RM: _calculated1RMs['pullup'] ?? 0, result1RM: _calculated1RMs['pullup'] ?? 0,
resultTM: _calculatedTMs['pullup'] ?? 0, resultTM: _calculatedTMs['pullup'] ?? 0,
onChanged: _calculateAll, onChanged: _calculateAll,
@ -196,10 +278,22 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
_calculateAll(); _calculateAll();
}); });
}, },
isAssisted: _isAssistedDip,
onToggleAssisted: (val) {
setState(() {
_isAssistedDip = val;
_calculateAll();
});
},
weightController: weightController:
_canDoDip ? _dipWeightController : _benchWeightController, _canDoDip ? _dipWeightController : _benchWeightController,
repsController: _pushRepsController, 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', repsLabel: 'Reps',
showWeightInput: true, showWeightInput: true,
showResults: true, showResults: true,
@ -295,7 +389,10 @@ class _ExerciseCard extends StatelessWidget {
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text(exerciseName, Text(exerciseName,
style: Theme.of(context).textTheme.titleLarge), style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(color: AppTheme.textPrimary)),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -357,6 +454,8 @@ class _AdaptiveExerciseCard extends StatelessWidget {
final double result1RM; final double result1RM;
final double resultTM; final double resultTM;
final VoidCallback onChanged; final VoidCallback onChanged;
final bool isAssisted;
final ValueChanged<bool>? onToggleAssisted;
const _AdaptiveExerciseCard({ const _AdaptiveExerciseCard({
required this.slotTitle, required this.slotTitle,
@ -374,6 +473,8 @@ class _AdaptiveExerciseCard extends StatelessWidget {
required this.result1RM, required this.result1RM,
required this.resultTM, required this.resultTM,
required this.onChanged, required this.onChanged,
this.isAssisted = false,
this.onToggleAssisted,
}); });
@override @override
@ -384,14 +485,15 @@ class _AdaptiveExerciseCard extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text(slotTitle.toUpperCase(),
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold)),
]),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(slotTitle.toUpperCase(),
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold)),
Row( Row(
children: [ children: [
Text('Can do 1 rep?', 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), const SizedBox(height: 4),
@ -421,13 +541,16 @@ class _AdaptiveExerciseCard extends StatelessWidget {
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text(isCapable ? primaryName : secondaryName, Text(isCapable ? primaryName : secondaryName,
style: Theme.of(context).textTheme.titleLarge), style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(color: AppTheme.textPrimary)),
], ],
), ),
if (!isCapable) ...[ if (!isCapable) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Adjusted Strategy: ${isCapable ? "Wendler 5/3/1" : "Linear Progression (3x5)"}', 'Adjusted: ${"Wendler 5/3/1"}',
style: const TextStyle( style: const TextStyle(
color: AppTheme.secondaryColor, color: AppTheme.secondaryColor,
fontSize: 12, fontSize: 12,

View file

@ -352,10 +352,14 @@ class _CurrentCycleCard extends StatelessWidget {
Text('Current Training Maxes (TM)', Text('Current Training Maxes (TM)',
style: Theme.of(context).textTheme.labelLarge), style: Theme.of(context).textTheme.labelLarge),
const SizedBox(height: 16), const SizedBox(height: 16),
_StatRow(label: 'Squat', value: '${tms['squat']} kg'),
_StatRow( _StatRow(
label: getLabel(pullVariant), value: '${tms['pullup']} kg'), label: 'Squat', value: '${tms['squat'].toStringAsFixed(2)} kg'),
_StatRow(label: getLabel(pushVariant), value: '${tms['dip']} 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), const SizedBox(height: 32),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
@ -468,15 +472,15 @@ class _DiffRow extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Expanded(child: Text(name)), Expanded(child: Text(name)),
Text('${oldVal.toStringAsFixed(1)}', Text('${oldVal.toStringAsFixed(2)}',
style: const TextStyle(color: Colors.grey)), style: const TextStyle(color: Colors.grey)),
Text( Text(
newVal.toStringAsFixed(1), newVal.toStringAsFixed(2),
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
if (isPositive) if (isPositive)
Text('+${diff.toStringAsFixed(1)}', Text('+${diff.toStringAsFixed(2)}',
style: const TextStyle( style: const TextStyle(
color: AppTheme.successColor, fontWeight: FontWeight.bold)) color: AppTheme.successColor, fontWeight: FontWeight.bold))
else else

View file

@ -67,22 +67,14 @@ class WorkoutGeneratorService {
List<WorkoutSet> sets; List<WorkoutSet> sets;
if (isMain) { if (isMain) {
if (type == ExerciseType.row || type == ExerciseType.bench) { sets = WendlerCalculator.generateSets(
sets = WendlerCalculator.generateLinearSets( week: week,
trainingMax: tm, trainingMax: tm,
exerciseType: type, exerciseType: type,
currentBodyweight: user.currentBodyweight); currentBodyweight: user.currentBodyweight,
} else { );
sets = WendlerCalculator.generateSets(
week: week,
trainingMax: tm,
exerciseType: type,
currentBodyweight: user.currentBodyweight,
);
}
} else { } else {
if (week == 4) return; if (week == 4) return;
if (type == ExerciseType.row || type == ExerciseType.bench) return;
sets = WendlerCalculator.generateFSLSets( sets = WendlerCalculator.generateFSLSets(
trainingMax: tm, trainingMax: tm,
@ -156,8 +148,8 @@ class WorkoutGeneratorService {
weight: calculateWeight(squatTm, 0.4))); weight: calculateWeight(squatTm, 0.4)));
accessories.add(_createIntervalExercise( accessories.add(_createIntervalExercise(
id: 'kb_swing', id: 'kb_snatch_acc',
name: '2H KB Swing', name: 'KB Snatch',
sets: 10, sets: 10,
intervalSeconds: 60, intervalSeconds: 60,
repsPerSet: 10)); repsPerSet: 10));
@ -178,8 +170,8 @@ class WorkoutGeneratorService {
weight: calculateWeight(pullupTm, 0.2))); weight: calculateWeight(pullupTm, 0.2)));
accessories.add(_createIntervalExercise( accessories.add(_createIntervalExercise(
id: 'kb_snatch_acc', id: 'kb_swing',
name: 'KB Snatch', name: '2H KB Swing',
sets: 10, sets: 10,
intervalSeconds: 60, intervalSeconds: 60,
repsPerSet: 5)); repsPerSet: 5));

View file

@ -19,6 +19,7 @@ import '../widgets/plate_visualizer.dart';
import '../widgets/enemy_hp_bar.dart'; import '../widgets/enemy_hp_bar.dart';
import '../../../gamification/application/quest_service.dart'; import '../../../gamification/application/quest_service.dart';
import '../widgets/emom_timer_widget.dart'; import '../widgets/emom_timer_widget.dart';
import '../widgets/timer_widget.dart';
class BattleScreen extends ConsumerStatefulWidget { class BattleScreen extends ConsumerStatefulWidget {
final int week; final int week;
@ -615,7 +616,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
Positioned.fill( Positioned.fill(
child: SafeArea( child: SafeArea(
child: _isResting child: _isResting
? _buildRestScreen() ? _buildRestScreen(inventory)
: _buildWorkoutScreen(currentExercise, currentSet, : _buildWorkoutScreen(currentExercise, currentSet,
plateResult, completedHP, totalHP), 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( return Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
@ -639,51 +653,151 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
), ),
), ),
child: Center( child: Center(
child: Column( child: SingleChildScrollView(
mainAxisAlignment: MainAxisAlignment.center, padding: const EdgeInsets.all(16),
children: [ child: Column(
Text( mainAxisAlignment: MainAxisAlignment.center,
'REST', mainAxisSize: MainAxisSize.min,
style: Theme.of(context).textTheme.displayLarge, children: [
), Text(
const SizedBox(height: 32), 'REST',
SizedBox( style: Theme.of(context).textTheme.displayLarge,
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,
),
),
],
), ),
), const SizedBox(height: 20),
const SizedBox(height: 48), SizedBox(
ElevatedButton( width: 200,
onPressed: _skipRest, height: 200,
child: const Text('SKIP REST'), 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( Widget _buildWorkoutScreen(
Exercise currentExercise, Exercise currentExercise,
WorkoutSet currentSet, WorkoutSet currentSet,

View file

@ -38,6 +38,24 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
_secondsRemaining = widget.intervalSeconds; _secondsRemaining = widget.intervalSeconds;
_audioPlayer = AudioPlayer(); _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( _pulseController = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),

View file

@ -238,7 +238,7 @@ class PlateVisualizer extends StatelessWidget {
if (isTwoSided) _buildBarbellView() else _buildBeltView(), if (isTwoSided) _buildBarbellView() else _buildBeltView(),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( 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( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
), ),

View file

@ -62,7 +62,9 @@ class WendlerCalculator {
final rounded = _roundWeight(targetTotal, exerciseType); final rounded = _roundWeight(targetTotal, exerciseType);
double plateWeight = 0; double plateWeight = 0;
if (exerciseType != ExerciseType.squat) { if (exerciseType != ExerciseType.squat ||
exerciseType != ExerciseType.row ||
exerciseType != ExerciseType.bench) {
plateWeight = max(0, rounded - currentBodyweight); plateWeight = max(0, rounded - currentBodyweight);
} }
@ -144,7 +146,9 @@ class WendlerCalculator {
final rounded = _roundWeight(targetTotal, exerciseType); final rounded = _roundWeight(targetTotal, exerciseType);
double plateWeight = 0; double plateWeight = 0;
if (exerciseType != ExerciseType.squat) { if (exerciseType != ExerciseType.squat ||
exerciseType != ExerciseType.row ||
exerciseType != ExerciseType.bench) {
plateWeight = max(0, rounded - currentBodyweight); plateWeight = max(0, rounded - currentBodyweight);
} }