diff --git a/.gitignore b/.gitignore index 3820a95..84f4bd6 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +.env +.env.production +.env.development diff --git a/lib/main.dart b/lib/main.dart index dc9f9da..df186ee 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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([ diff --git a/lib/src/features/authentication/presentation/screens/profile_screen.dart b/lib/src/features/authentication/presentation/screens/profile_screen.dart index 43eaa5e..a4ca385 100644 --- a/lib/src/features/authentication/presentation/screens/profile_screen.dart +++ b/lib/src/features/authentication/presentation/screens/profile_screen.dart @@ -425,199 +425,204 @@ class _ProfileScreenState extends ConsumerState { ), 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, + ) + ], + ), ), ); } diff --git a/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart b/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart index a9e877d..1da8254 100644 --- a/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart @@ -32,6 +32,9 @@ class _StrengthTestScreenState extends ConsumerState { Map _calculated1RMs = {}; Map _calculatedTMs = {}; + bool _isAssistedPull = false; + bool _isAssistedDip = false; + @override void initState() { super.initState(); @@ -52,16 +55,27 @@ class _StrengthTestScreenState extends ConsumerState { 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 { } 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 { }); } + // 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 { _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 { _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? 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, diff --git a/lib/src/features/stats/presentation/screens/stats_screen.dart b/lib/src/features/stats/presentation/screens/stats_screen.dart index dce18a7..0589d2d 100644 --- a/lib/src/features/stats/presentation/screens/stats_screen.dart +++ b/lib/src/features/stats/presentation/screens/stats_screen.dart @@ -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 diff --git a/lib/src/features/workout_runner/application/workout_generator_service.dart b/lib/src/features/workout_runner/application/workout_generator_service.dart index 756d2b9..c2ae329 100644 --- a/lib/src/features/workout_runner/application/workout_generator_service.dart +++ b/lib/src/features/workout_runner/application/workout_generator_service.dart @@ -67,22 +67,14 @@ class WorkoutGeneratorService { List 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)); diff --git a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart index e085327..44ffa4e 100644 --- a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart +++ b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart @@ -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 { Positioned.fill( child: SafeArea( child: _isResting - ? _buildRestScreen() + ? _buildRestScreen(inventory) : _buildWorkoutScreen(currentExercise, currentSet, plateResult, completedHP, totalHP), ), @@ -626,7 +627,20 @@ class _BattleScreenState extends ConsumerState { }); } - Widget _buildRestScreen() { + Widget _buildRestScreen(Map 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 { ), ), 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 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 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, diff --git a/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart b/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart index 68c61e4..e81a43e 100644 --- a/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart +++ b/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart @@ -38,6 +38,24 @@ class _EmomTimerWidgetState extends State _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), diff --git a/lib/src/features/workout_runner/presentation/widgets/plate_visualizer.dart b/lib/src/features/workout_runner/presentation/widgets/plate_visualizer.dart index 062b0c7..9d95f41 100644 --- a/lib/src/features/workout_runner/presentation/widgets/plate_visualizer.dart +++ b/lib/src/features/workout_runner/presentation/widgets/plate_visualizer.dart @@ -238,7 +238,7 @@ class PlateVisualizer extends StatelessWidget { if (isTwoSided) _buildBarbellView() else _buildBeltView(), const SizedBox(height: 16), Text( - 'Total: ${plateConfiguration.fold(0, (sum, p) => sum + p).toStringAsFixed(1)} kg ${isTwoSided ? 'per side' : ''}', + 'Total: ${plateConfiguration.fold(0, (sum, p) => sum + p).toStringAsFixed(2)} kg ${isTwoSided ? 'per side' : ''}', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.primaryColor, ), diff --git a/lib/src/shared/domain/logic/wendler_calculator.dart b/lib/src/shared/domain/logic/wendler_calculator.dart index 1533b06..b1b7059 100644 --- a/lib/src/shared/domain/logic/wendler_calculator.dart +++ b/lib/src/shared/domain/logic/wendler_calculator.dart @@ -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); }