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

View file

@ -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([

View file

@ -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,
)
],
),
),
);
}

View file

@ -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,

View file

@ -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

View file

@ -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));

View file

@ -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,

View file

@ -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),

View file

@ -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,
),

View file

@ -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);
}