From 32e935b3ec38fc95da54990fa79699a733df07c3 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Sun, 4 Jan 2026 10:56:06 +0100 Subject: [PATCH] refactor: clean up smaller fixes --- assets/audio/beep_long.ogg | Bin 0 -> 25566 bytes assets/audio/beep_short.ogg | Bin 0 -> 7286 bytes lib/src/core/constants/app_constants.dart | 6 +- lib/src/core/constants/asset_paths.dart | 3 + lib/src/core/routing/app_router.dart | 6 +- lib/src/core/theme/app_theme.dart | 6 +- .../presentation/screens/profile_screen.dart | 151 ++- .../presentation/screens/hub_screen.dart | 210 ++-- .../presentation/widgets/level_display.dart | 3 +- .../widgets/start_raid_button.dart | 4 +- .../presentation/widgets/xp_bar_widget.dart | 23 +- .../presentation/screens/codex_screen.dart | 6 +- .../presentation/widgets/avatar_renderer.dart | 2 +- .../presentation/widgets/quest_item.dart | 89 +- .../presentation/screens/history_screen.dart | 6 +- .../screens/inventory_screen.dart | 9 +- .../presentation/widgets/plate_counter.dart | 6 +- .../screens/inventory_setup_screen.dart | 3 +- .../screens/strength_test_screen.dart | 14 +- .../presentation/screens/welcome_screen.dart | 8 +- .../presentation/screens/stats_screen.dart | 9 +- .../presentation/widgets/progress_chart.dart | 6 +- .../workout_generator_service.dart | 256 +++++ .../presentation/screens/battle_screen.dart | 410 ++++++- .../presentation/screens/battle_screen_back | 1011 ----------------- .../widgets/emom_timer_widget.dart | 232 ++++ .../presentation/widgets/enemy_hp_bar.dart | 4 +- .../widgets/plate_visualizer.dart | 2 +- .../data/repositories/workout_repository.dart | 111 +- lib/src/shared/domain/entities/exercise.dart | 2 +- .../domain/logic/wendler_calculator.dart | 25 +- pubspec.lock | 56 + pubspec.yaml | 2 + 33 files changed, 1292 insertions(+), 1389 deletions(-) create mode 100644 assets/audio/beep_long.ogg create mode 100644 assets/audio/beep_short.ogg create mode 100644 lib/src/features/workout_runner/application/workout_generator_service.dart delete mode 100644 lib/src/features/workout_runner/presentation/screens/battle_screen_back create mode 100644 lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart diff --git a/assets/audio/beep_long.ogg b/assets/audio/beep_long.ogg new file mode 100644 index 0000000000000000000000000000000000000000..1fe6edff924d9b87ad90a3a15c80de9bbd69da37 GIT binary patch literal 25566 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 S;ZXapp8MPF?h3qgXFCAnHQ<~8 literal 0 HcmV?d00001 diff --git a/assets/audio/beep_short.ogg b/assets/audio/beep_short.ogg new file mode 100644 index 0000000000000000000000000000000000000000..73ef94a9fdd4a0dc5e5fbc9b9786b349da581d68 GIT binary patch literal 7286 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 literal 0 HcmV?d00001 diff --git a/lib/src/core/constants/app_constants.dart b/lib/src/core/constants/app_constants.dart index e2c9d81..21d0838 100644 --- a/lib/src/core/constants/app_constants.dart +++ b/lib/src/core/constants/app_constants.dart @@ -13,14 +13,14 @@ class AppConstants { // XP System static const int baseXP = 1000; - static const double xpMultiplier = 1.15; + static const double xpMultiplier = 1.25; static const int maxLevel = 100; // XP Rewards static const int workoutCompleteXP = 100; - static const double volumeXPRate = 0.1; // XP per kg + static const double volumeXPRate = 0.01; // XP per kg static const int amrapBonusXPPerRep = 25; - static const int prBonusXP = 500; + static const int prBonusXP = 200; static const int cycleCompleteXP = 500; // Rounding Steps diff --git a/lib/src/core/constants/asset_paths.dart b/lib/src/core/constants/asset_paths.dart index a86cabb..c34b4e6 100644 --- a/lib/src/core/constants/asset_paths.dart +++ b/lib/src/core/constants/asset_paths.dart @@ -43,6 +43,9 @@ class AssetPaths { static String getAvatarPath(String gender, int variant) { return 'assets/images/avatars/$gender/$variant.png'; } + + static const String audioBeepShort = 'audio/beep_short.ogg'; + static const String audioBeepLong = 'audio/beep_long.ogg'; } class PlateColors { diff --git a/lib/src/core/routing/app_router.dart b/lib/src/core/routing/app_router.dart index cfadaff..d01db62 100644 --- a/lib/src/core/routing/app_router.dart +++ b/lib/src/core/routing/app_router.dart @@ -165,7 +165,7 @@ class _SplashScreenState extends ConsumerState { ), Positioned.fill( child: Container( - color: Colors.black.withOpacity(0.5), + color: Colors.black.withValues(alpha: 0.5), ), ), Center( @@ -176,11 +176,11 @@ class _SplashScreenState extends ConsumerState { width: 120, height: 120, decoration: BoxDecoration( - color: const Color(0xFF00E5FF).withOpacity(0.9), + color: const Color(0xFF00E5FF).withValues(alpha: 0.9), borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( - color: const Color(0xFF00E5FF).withOpacity(0.6), + color: const Color(0xFF00E5FF).withValues(alpha: 0.6), blurRadius: 20, spreadRadius: 5, ), diff --git a/lib/src/core/theme/app_theme.dart b/lib/src/core/theme/app_theme.dart index 104f8e0..a916685 100644 --- a/lib/src/core/theme/app_theme.dart +++ b/lib/src/core/theme/app_theme.dart @@ -87,7 +87,7 @@ class AppTheme { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide( - color: primaryColor.withOpacity(0.3), + color: primaryColor.withValues(alpha: 0.3), width: 1, ), ), @@ -97,11 +97,11 @@ class AppTheme { fillColor: surfaceColor, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: primaryColor.withOpacity(0.5)), + borderSide: BorderSide(color: primaryColor.withValues(alpha: 0.5)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: primaryColor.withOpacity(0.3)), + borderSide: BorderSide(color: primaryColor.withValues(alpha: 0.3)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), diff --git a/lib/src/features/authentication/presentation/screens/profile_screen.dart b/lib/src/features/authentication/presentation/screens/profile_screen.dart index 709964e..6205cd7 100644 --- a/lib/src/features/authentication/presentation/screens/profile_screen.dart +++ b/lib/src/features/authentication/presentation/screens/profile_screen.dart @@ -10,6 +10,7 @@ import '../../../gamification/domain/entities/avatar_config.dart'; import '../../../gamification/presentation/widgets/avatar_editor.dart'; import '../../../gamification/presentation/widgets/avatar_renderer.dart'; import '../../../gamification/domain/entities/item_catalog.dart'; +import '../../../../shared/domain/logic/wendler_calculator.dart'; class ProfileScreen extends ConsumerStatefulWidget { const ProfileScreen({super.key}); @@ -176,11 +177,10 @@ class _ProfileScreenState extends ConsumerState { Navigator.pop(context); setState(() => _isLoading = true); - // Update Config final newConfig = AvatarConfig( gender: currentConfig.gender, variant: currentConfig.variant, - selectedBackground: item.id, // Hintergrund setzen + selectedBackground: item.id, ); final updatedUser = _user!.copyWith( @@ -193,12 +193,6 @@ class _ProfileScreenState extends ConsumerState { .read(userRepositoryProvider) .saveLocalUser(_user!); setState(() => _isLoading = false); - // Save to DB - // await ref - // .read(userRepositoryProvider) - // .updateAvatarConfig(newConfig.toJson()); - // await _loadUser(); - // setState(() => _isLoading = false); } : null, child: Container( @@ -256,7 +250,7 @@ class _ProfileScreenState extends ConsumerState { child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), + color: Colors.black.withValues(alpha: 0.7), borderRadius: const BorderRadius.vertical( bottom: Radius.circular(10)), ), @@ -358,6 +352,48 @@ class _ProfileScreenState extends ConsumerState { }); } + AccessoryTemplate _getTemplateFromSettings(Map settings) { + final key = settings['accessory_template'] as String?; + if (key == 'hypertrophy') return AccessoryTemplate.hypertrophy; + if (key == 'conditioning') return AccessoryTemplate.conditioning; + return AccessoryTemplate.none; + } + + Future _updateTemplate(AccessoryTemplate newTemplate) async { + setState(() => _isLoading = true); + + String templateKey = 'none'; + if (newTemplate == AccessoryTemplate.hypertrophy) { + templateKey = 'hypertrophy'; + } + if (newTemplate == AccessoryTemplate.conditioning) { + templateKey = 'conditioning'; + } + + final currentSettings = + Map.from(_user!.inventorySettings ?? {}); + currentSettings['accessory_template'] = templateKey; + + try { + final updatedUser = _user!.copyWith( + inventorySettings: Value(currentSettings), + isDirty: true, + ); + + await ref.read(userRepositoryProvider).saveLocalUser(updatedUser); + + ref.read(userRepositoryProvider).updateInventory(currentSettings); + + setState(() { + _user = updatedUser; + _isLoading = false; + }); + } catch (e) { + setState(() => _isLoading = false); + // Error handling... + } + } + @override Widget build(BuildContext context) { final userRepo = ref.watch(userRepositoryProvider); @@ -411,7 +447,6 @@ class _ProfileScreenState extends ConsumerState { ), ), const SizedBox(height: 32), - // const SizedBox(height: 16), Center( child: OutlinedButton.icon( onPressed: _showBackgroundSelector, @@ -441,7 +476,7 @@ class _ProfileScreenState extends ConsumerState { value: _currentBodyweight, min: 40, max: 150, - divisions: 220, // 0.5 steps + divisions: 220, label: _currentBodyweight.toStringAsFixed(1), activeColor: AppTheme.primaryColor, onChanged: (val) => setState(() { @@ -467,6 +502,26 @@ class _ProfileScreenState extends ConsumerState { ), ), 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 @@ -489,10 +544,10 @@ class _ProfileScreenState extends ConsumerState { const SizedBox(height: 8), Container( decoration: BoxDecoration( - border: - Border.all(color: AppTheme.errorColor.withOpacity(0.5)), + border: Border.all( + color: AppTheme.errorColor.withValues(alpha: 0.5)), borderRadius: BorderRadius.circular(12), - color: AppTheme.errorColor.withOpacity(0.05), + color: AppTheme.errorColor.withValues(alpha: 0.05), ), child: Column( children: [ @@ -562,4 +617,72 @@ class _ProfileScreenState extends ConsumerState { ), ); } + + Widget _buildTemplateSelector() { + final current = _getTemplateFromSettings(_user?.inventorySettings ?? {}); + + return Column( + children: [ + _RadioTile( + value: AccessoryTemplate.none, + groupValue: current, + title: 'Strength Only', + subtitle: 'Main Lifts + FSL. Pure & Fast.', + onChanged: (val) => _updateTemplate(val!), + ), + const Divider(height: 1), + _RadioTile( + value: AccessoryTemplate.hypertrophy, + groupValue: current, + title: 'Hypertrophy Support', + subtitle: 'Bodybuilding accessories to build muscle armor.', + onChanged: (val) => _updateTemplate(val!), + ), + const Divider(height: 1), + _RadioTile( + value: AccessoryTemplate.conditioning, + groupValue: current, + title: 'The Engine (Conditioning)', + subtitle: '15 min Kettlebell intervals to boost stamina.', + onChanged: (val) => _updateTemplate(val!), + ), + ], + ); + } +} + +class _RadioTile extends StatelessWidget { + final T value; + final T groupValue; + final String title; + final String subtitle; + final ValueChanged onChanged; + + const _RadioTile({ + required this.value, + required this.groupValue, + required this.title, + required this.subtitle, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final isSelected = value == groupValue; + return RadioListTile( + value: value, + groupValue: groupValue, + onChanged: onChanged, + activeColor: AppTheme.primaryColor, + title: Text( + title, + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? AppTheme.primaryColor : Colors.white, + ), + ), + subtitle: Text(subtitle, style: const TextStyle(fontSize: 12)), + contentPadding: EdgeInsets.zero, + ); + } } diff --git a/lib/src/features/dashboard/presentation/screens/hub_screen.dart b/lib/src/features/dashboard/presentation/screens/hub_screen.dart index ead253b..f1fd278 100644 --- a/lib/src/features/dashboard/presentation/screens/hub_screen.dart +++ b/lib/src/features/dashboard/presentation/screens/hub_screen.dart @@ -1,10 +1,7 @@ -// import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -// import '../../../../core/constants/asset_paths.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../shared/data/local/app_database.dart'; import '../../../../shared/data/repositories/user_repository.dart'; @@ -23,6 +20,7 @@ import '../widgets/xp_bar_widget.dart'; import '../widgets/level_display.dart'; import '../widgets/start_raid_button.dart'; import '../../../gamification/application/quest_service.dart'; +import '../../../workout_runner/application/workout_generator_service.dart'; class HubScreen extends ConsumerStatefulWidget { const HubScreen({super.key}); @@ -49,102 +47,11 @@ class _HubScreenState extends ConsumerState { }); } - List _generateExercises({ - required int week, - required int day, - required Map trainingMaxes, - required double bodyweight, - required UserCollection user, - }) { - final exercises = []; - final variants = user.exerciseVariants ?? {}; - - (String, String, ExerciseType) resolveVariant(String slot, String defaultId, - String defaultName, ExerciseType defaultType) { - final variant = variants[slot]; - - if (slot == 'pull') { - if (variant == 'row') return ('row', 'Pendlay Row', ExerciseType.row); - return ('pullup', 'Weighted Pull-up', ExerciseType.pullup); - } - - if (slot == 'push') { - if (variant == 'bench') { - return ('bench', 'Bench Press', ExerciseType.bench); - } - return ('dip', 'Weighted Dip', ExerciseType.dip); - } - - return (defaultId, defaultName, defaultType); - } - - void addExercise(String slot, String defaultId, String defaultName, - ExerciseType defaultType, bool isMain) { - final (id, name, type) = - resolveVariant(slot, defaultId, defaultName, defaultType); - - final tmKey = defaultId; - final tm = trainingMaxes[tmKey] ?? 0.0; - 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, - ); - } - } else { - if (week == 4) { - return; - } - - if (type == ExerciseType.row || type == ExerciseType.bench) return; - - sets = WendlerCalculator.generateFSLSets( - trainingMax: tm, - exerciseType: type, - currentBodyweight: user.currentBodyweight, - ); - } - - if (sets.isNotEmpty) { - exercises.add(Exercise( - exerciseId: id, - exerciseName: isMain ? name : '$name (FSL)', - bodyweightAtSession: user.currentBodyweight, - sets: sets, - )); - } - } - - if (day == 1) { - addExercise('legs', 'squat', 'Back Squat', ExerciseType.squat, true); - addExercise( - 'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, false); - } else if (day == 2) { - addExercise('push', 'dip', 'Weighted Dip', ExerciseType.dip, true); - addExercise('legs', 'squat', 'Back Squat', ExerciseType.squat, false); - } else if (day == 3) { - addExercise( - 'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, true); - addExercise('push', 'dip', 'Weighted Dip', ExerciseType.dip, false); - } - - return exercises; - } - Future _startNextWorkout( CycleCollection cycle, UserCollection user) async { try { final workoutRepo = ref.read(workoutRepositoryProvider); + final workoutGenerator = ref.read(workoutGeneratorServiceProvider); final tmsDynamic = cycle.trainingMaxes; final trainingMaxes = Map.from(tmsDynamic @@ -181,7 +88,6 @@ class _HubScreenState extends ConsumerState { } return; } - var workout = await workoutRepo.getWorkoutByWeekDay( cycleId: cycleRefId, localCycleId: localCycleId, @@ -190,12 +96,22 @@ class _HubScreenState extends ConsumerState { ); if (workout == null) { - final exercises = _generateExercises( - week: targetWeek, - day: targetDay, - trainingMaxes: trainingMaxes, - bodyweight: user.currentBodyweight, - user: user); + final activeTemplate = _getTemplateFromUser(user); + int? conditioningSets; + + if (activeTemplate == AccessoryTemplate.conditioning) { + conditioningSets = await _showConditioningDialog(); + if (conditioningSets == null) return; + } + + final exercises = workoutGenerator.generateWorkout( + week: targetWeek, + day: targetDay, + trainingMaxes: trainingMaxes, + user: user, + template: activeTemplate, + conditioningSets: conditioningSets, + ); final userId = user.serverId ?? user.id.toString(); @@ -225,6 +141,88 @@ class _HubScreenState extends ConsumerState { } } + AccessoryTemplate _getTemplateFromUser(UserCollection user) { + final settings = user.inventorySettings ?? {}; + final key = settings['accessory_template'] as String?; + if (key == 'hypertrophy') return AccessoryTemplate.hypertrophy; + if (key == 'conditioning') return AccessoryTemplate.conditioning; + return AccessoryTemplate.none; + } + + Future _showConditioningDialog() async { + int sets = 20; + + return await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return StatefulBuilder( + builder: (context, setDialogState) { + final interval = (20 * 60) / sets; + + return AlertDialog( + title: const Text( + 'MISSION BRIEFING', + style: TextStyle( + color: AppTheme.textSecondary, + fontWeight: FontWeight.bold, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'The enemy is fleeing! We have a 20-minute window to intercept.', + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + Text( + 'Combat Density: $sets Sets', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold), + ), + Text( + 'Interval: Every ${interval.toStringAsFixed(0)} seconds', + style: const TextStyle(color: Colors.grey), + ), + const SizedBox(height: 16), + Slider( + value: sets.toDouble(), + min: 10, + max: 30, + divisions: 20, + activeColor: AppTheme.primaryColor, + onChanged: (val) { + setDialogState(() => sets = val.toInt()); + }, + ), + const SizedBox(height: 8), + if (sets >= 20) + const Text('⚠️ HARDCORE MODE', + style: TextStyle( + color: AppTheme.errorColor, + fontSize: 10, + fontWeight: FontWeight.bold)), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, null), + child: const Text('ABORT'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, sets), + child: const Text('ENGAGE'), + ), + ], + ); + }, + ); + }, + ); + } + @override Widget build(BuildContext context) { final userRepo = ref.watch(userRepositoryProvider); @@ -280,9 +278,7 @@ class _HubScreenState extends ConsumerState { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - // Colors.black.withOpacity(0.6), Colors.black.withValues(alpha: 0.6), - // Colors.black.withOpacity(0.85), Colors.black.withValues(alpha: 0.85), ], ), @@ -389,7 +385,6 @@ class _HubScreenState extends ConsumerState { top: Radius.circular(24)), boxShadow: [ BoxShadow( - // color: Colors.black.withOpacity(0.2), color: Colors.black.withValues(alpha: 0.2), blurRadius: 10, offset: const Offset(0, -5), @@ -435,10 +430,6 @@ class _HubScreenState extends ConsumerState { } } -// extension on Object { -// operator [](String other) {} -// } - class _StatBox extends StatelessWidget { final String label; final String value; @@ -456,7 +447,6 @@ class _StatBox extends StatelessWidget { color: AppTheme.surfaceColor, borderRadius: BorderRadius.circular(12), border: Border.all( - // color: AppTheme.primaryColor.withOpacity(0.3), color: AppTheme.primaryColor.withValues(alpha: 0.3), ), ), diff --git a/lib/src/features/dashboard/presentation/widgets/level_display.dart b/lib/src/features/dashboard/presentation/widgets/level_display.dart index 73a651c..9eb8cab 100644 --- a/lib/src/features/dashboard/presentation/widgets/level_display.dart +++ b/lib/src/features/dashboard/presentation/widgets/level_display.dart @@ -23,7 +23,7 @@ class LevelDisplay extends StatelessWidget { borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( - color: AppTheme.primaryColor.withOpacity(0.5), + color: AppTheme.primaryColor.withValues(alpha: 0.5), blurRadius: 12, spreadRadius: 2, ), @@ -57,4 +57,3 @@ class LevelDisplay extends StatelessWidget { ); } } - diff --git a/lib/src/features/dashboard/presentation/widgets/start_raid_button.dart b/lib/src/features/dashboard/presentation/widgets/start_raid_button.dart index e46d41e..ecb179f 100644 --- a/lib/src/features/dashboard/presentation/widgets/start_raid_button.dart +++ b/lib/src/features/dashboard/presentation/widgets/start_raid_button.dart @@ -54,7 +54,8 @@ class _StartRaidButtonState extends State borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: AppTheme.primaryColor.withOpacity(_glowAnimation.value), + color: AppTheme.primaryColor + .withValues(alpha: _glowAnimation.value), blurRadius: 20, spreadRadius: 5, ), @@ -96,4 +97,3 @@ class _StartRaidButtonState extends State ); } } - diff --git a/lib/src/features/dashboard/presentation/widgets/xp_bar_widget.dart b/lib/src/features/dashboard/presentation/widgets/xp_bar_widget.dart index 3b5e5fb..57e5245 100644 --- a/lib/src/features/dashboard/presentation/widgets/xp_bar_widget.dart +++ b/lib/src/features/dashboard/presentation/widgets/xp_bar_widget.dart @@ -49,7 +49,7 @@ class XPBarWidget extends StatelessWidget { color: AppTheme.xpBarBackground, borderRadius: BorderRadius.circular(16), border: Border.all( - color: AppTheme.primaryColor.withOpacity(0.3), + color: AppTheme.primaryColor.withValues(alpha: 0.3), width: 2, ), ), @@ -64,13 +64,13 @@ class XPBarWidget extends StatelessWidget { gradient: LinearGradient( colors: [ AppTheme.primaryColor, - AppTheme.primaryColor.withOpacity(0.7), + AppTheme.primaryColor.withValues(alpha: 0.7), ], ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: AppTheme.primaryColor.withOpacity(0.5), + color: AppTheme.primaryColor.withValues(alpha: 0.5), blurRadius: 8, spreadRadius: 1, ), @@ -86,15 +86,15 @@ class XPBarWidget extends StatelessWidget { child: Text( '${(progress * 100).toStringAsFixed(0)}%', style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.white, - shadows: [ - const Shadow( - color: Colors.black, - blurRadius: 4, - ), - ], + fontWeight: FontWeight.bold, + color: Colors.white, + shadows: [ + const Shadow( + color: Colors.black, + blurRadius: 4, ), + ], + ), ), ), ], @@ -103,4 +103,3 @@ class XPBarWidget extends StatelessWidget { ); } } - diff --git a/lib/src/features/gamification/presentation/screens/codex_screen.dart b/lib/src/features/gamification/presentation/screens/codex_screen.dart index 5b7b478..fa10162 100644 --- a/lib/src/features/gamification/presentation/screens/codex_screen.dart +++ b/lib/src/features/gamification/presentation/screens/codex_screen.dart @@ -83,14 +83,14 @@ class _LoreCard extends StatelessWidget { clipBehavior: Clip.antiAlias, child: Container( decoration: BoxDecoration( - border: Border.all(color: color.withOpacity(0.5), width: 1), + border: Border.all(color: color.withValues(alpha: 0.5), width: 1), borderRadius: BorderRadius.circular(16), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ AppTheme.surfaceColor, - color.withOpacity(0.1), + color.withValues(alpha: 0.1), ], ), ), @@ -112,7 +112,7 @@ class _LoreCard extends StatelessWidget { child: Image.asset( assetPath, fit: BoxFit.contain, - color: Colors.white.withOpacity(0.9), + color: Colors.white.withValues(alpha: 0.9), colorBlendMode: BlendMode.modulate, ), ), diff --git a/lib/src/features/gamification/presentation/widgets/avatar_renderer.dart b/lib/src/features/gamification/presentation/widgets/avatar_renderer.dart index 7bd9aa8..faf9b32 100644 --- a/lib/src/features/gamification/presentation/widgets/avatar_renderer.dart +++ b/lib/src/features/gamification/presentation/widgets/avatar_renderer.dart @@ -20,7 +20,7 @@ class AvatarRenderer extends StatelessWidget { shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), blurRadius: 10, spreadRadius: 2, ), diff --git a/lib/src/features/gamification/presentation/widgets/quest_item.dart b/lib/src/features/gamification/presentation/widgets/quest_item.dart index 37611e4..6704ac5 100644 --- a/lib/src/features/gamification/presentation/widgets/quest_item.dart +++ b/lib/src/features/gamification/presentation/widgets/quest_item.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/theme/app_theme.dart'; -import '../../../../shared/data/local/app_database.dart'; // Für QuestCollection Klasse +import '../../../../shared/data/local/app_database.dart'; import '../../data/repositories/quest_repository.dart'; import '../../domain/entities/item_catalog.dart'; @@ -23,19 +23,9 @@ class _QuestItemState extends ConsumerState { Future _handleClaim() async { setState(() => _isClaiming = true); try { - // 1. XP und Item gutschreiben (Logik im Repo oder Service wäre besser, - // aber für MVP machen wir den Claim im Repo und User-Update hier oder im Service). - // Einfachheitshalber: Repo setzt isClaimed=true. Wir müssen aber auch XP geben. - // BESSER: Wir nutzen einen QuestService Methode 'claimReward', die beides macht. - // Da wir die noch nicht haben, machen wir es hier "manuell" via Repos. - final questRepo = ref.read(questRepositoryProvider); await questRepo.claimQuest(widget.quest.id); - - // Wir verlassen uns darauf, dass der UserRepo/XP Service das separat regelt - // oder wir feuern hier ein Event. - // Für das UI Feedback reicht erst mal das Claimen. - + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -52,7 +42,8 @@ class _QuestItemState extends ConsumerState { } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e'))); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('Error: $e'))); } } finally { if (mounted) setState(() => _isClaiming = false); @@ -61,7 +52,8 @@ class _QuestItemState extends ConsumerState { @override Widget build(BuildContext context) { - final progress = (widget.quest.currentValue / widget.quest.targetValue).clamp(0.0, 1.0); + final progress = + (widget.quest.currentValue / widget.quest.targetValue).clamp(0.0, 1.0); final isComplete = widget.quest.isCompleted; final isClaimed = widget.quest.isClaimed; @@ -69,7 +61,7 @@ class _QuestItemState extends ConsumerState { margin: const EdgeInsets.only(bottom: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - side: isComplete && !isClaimed + side: isComplete && !isClaimed ? const BorderSide(color: AppTheme.successColor, width: 1) : BorderSide.none, ), @@ -83,7 +75,9 @@ class _QuestItemState extends ConsumerState { children: [ Icon( _getIconForType(widget.quest.type), - color: isComplete ? AppTheme.successColor : AppTheme.primaryColor, + color: isComplete + ? AppTheme.successColor + : AppTheme.primaryColor, size: 20, ), const SizedBox(width: 8), @@ -91,37 +85,40 @@ class _QuestItemState extends ConsumerState { child: Text( widget.quest.title, style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: isClaimed ? Colors.grey : Colors.white, - decoration: isClaimed ? TextDecoration.lineThrough : null, - ), + fontWeight: FontWeight.bold, + color: isClaimed ? Colors.grey : Colors.white, + decoration: + isClaimed ? TextDecoration.lineThrough : null, + ), ), ), if (isClaimed) const Icon(Icons.check, color: Colors.grey, size: 20) else if (widget.quest.rewardXP > 0) Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: AppTheme.xpBarFill.withOpacity(0.2), + color: AppTheme.xpBarFill.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), child: Text( '+${widget.quest.rewardXP} XP', style: const TextStyle( - color: AppTheme.primaryColor, - fontSize: 12, - fontWeight: FontWeight.bold - ), + color: AppTheme.primaryColor, + fontSize: 12, + fontWeight: FontWeight.bold), ), ), ], ), - + const SizedBox(height: 8), Text( widget.quest.description, - style: TextStyle(color: isClaimed ? Colors.grey : AppTheme.textSecondary, fontSize: 12), + style: TextStyle( + color: isClaimed ? Colors.grey : AppTheme.textSecondary, + fontSize: 12), ), const SizedBox(height: 16), @@ -137,34 +134,42 @@ class _QuestItemState extends ConsumerState { child: LinearProgressIndicator( value: progress, backgroundColor: Colors.grey[800], - color: isComplete ? AppTheme.successColor : AppTheme.primaryColor, + color: isComplete + ? AppTheme.successColor + : AppTheme.primaryColor, minHeight: 8, ), ), const SizedBox(height: 4), Text( '${widget.quest.currentValue} / ${widget.quest.targetValue}', - style: const TextStyle(color: Colors.grey, fontSize: 10), + style: + const TextStyle(color: Colors.grey, fontSize: 10), ), ], ), ), const SizedBox(width: 16), - if (isComplete && !isClaimed) ElevatedButton( onPressed: _isClaiming ? null : _handleClaim, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.successColor, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 0), minimumSize: const Size(0, 32), ), - child: _isClaiming - ? const SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) - : const Text('CLAIM', style: TextStyle(fontSize: 12)), + child: _isClaiming + ? const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + strokeWidth: 2, color: Colors.white)) + : const Text('CLAIM', style: TextStyle(fontSize: 12)), ) else if (widget.quest.rewardItem != null && !isClaimed) - const Icon(Icons.inventory_2, color: AppTheme.secondaryColor, size: 20), + const Icon(Icons.inventory_2, + color: AppTheme.secondaryColor, size: 20), ], ), ], @@ -175,10 +180,14 @@ class _QuestItemState extends ConsumerState { IconData _getIconForType(String type) { switch (type) { - case 'daily': return Icons.today; - case 'story': return Icons.auto_stories; - case 'milestone': return Icons.emoji_events; - default: return Icons.task_alt; + case 'daily': + return Icons.today; + case 'story': + return Icons.auto_stories; + case 'milestone': + return Icons.emoji_events; + default: + return Icons.task_alt; } } } diff --git a/lib/src/features/history/presentation/screens/history_screen.dart b/lib/src/features/history/presentation/screens/history_screen.dart index 305127d..ad0cc4f 100644 --- a/lib/src/features/history/presentation/screens/history_screen.dart +++ b/lib/src/features/history/presentation/screens/history_screen.dart @@ -53,7 +53,7 @@ class _HistoryScreenState extends ConsumerState { Icon( Icons.history_edu, size: 80, - color: AppTheme.primaryColor.withOpacity(0.5), + color: AppTheme.primaryColor.withValues(alpha: 0.5), ), const SizedBox(height: 16), Text( @@ -180,9 +180,9 @@ class _WorkoutHistoryCard extends StatelessWidget { width: 50, height: 50, decoration: BoxDecoration( - color: AppTheme.primaryColor.withOpacity(0.1), + color: AppTheme.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppTheme.primaryColor.withOpacity(0.3)), + border: Border.all(color: AppTheme.primaryColor.withValues(alpha: 0.3)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/src/features/inventory/presentation/screens/inventory_screen.dart b/lib/src/features/inventory/presentation/screens/inventory_screen.dart index 440a331..21d64a2 100644 --- a/lib/src/features/inventory/presentation/screens/inventory_screen.dart +++ b/lib/src/features/inventory/presentation/screens/inventory_screen.dart @@ -127,7 +127,9 @@ class _InventoryScreenState extends ConsumerState { final platesList = []; _plateInventory.forEach((weight, count) { - for (int i = 0; i < count; i++) platesList.add(weight); + for (int i = 0; i < count; i++) { + platesList.add(weight); + } }); final bandsList = >[]; @@ -258,7 +260,7 @@ class _InventoryScreenState extends ConsumerState { ?.copyWith(color: AppTheme.textSecondary)), const SizedBox(height: 8), SingleChildScrollView( - scrollDirection: Axis.horizontal, + scrollDirection: Axis.vertical, child: Row( children: [ ActionChip( @@ -315,7 +317,8 @@ class _InventoryScreenState extends ConsumerState { _hasChanges = true; }); }, - selectedColor: _getBandColor(entry.key).withOpacity(0.3), + selectedColor: + _getBandColor(entry.key).withValues(alpha: 0.3), checkmarkColor: _getBandColor(entry.key), side: BorderSide(color: _getBandColor(entry.key)), ); diff --git a/lib/src/features/inventory/presentation/widgets/plate_counter.dart b/lib/src/features/inventory/presentation/widgets/plate_counter.dart index 63e44c3..64d3529 100644 --- a/lib/src/features/inventory/presentation/widgets/plate_counter.dart +++ b/lib/src/features/inventory/presentation/widgets/plate_counter.dart @@ -68,10 +68,10 @@ class PlateCounter extends StatelessWidget { height: 40, alignment: Alignment.center, decoration: BoxDecoration( - color: AppTheme.primaryColor.withOpacity(0.1), + color: AppTheme.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), - border: - Border.all(color: AppTheme.primaryColor.withOpacity(0.3)), + border: Border.all( + color: AppTheme.primaryColor.withValues(alpha: 0.3)), ), child: Text( count.toString(), diff --git a/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart b/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart index 964dc1d..9cc5de9 100644 --- a/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart @@ -353,7 +353,8 @@ class _InventorySetupScreenState extends ConsumerState { _bandInventory[entry.key] = selected; }); }, - selectedColor: _getBandColor(entry.key).withOpacity(0.3), + selectedColor: + _getBandColor(entry.key).withValues(alpha: 0.3), checkmarkColor: _getBandColor(entry.key), labelStyle: TextStyle( color: entry.value ? Colors.white : Colors.grey, 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 f4c64c1..fa6a77b 100644 --- a/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart @@ -212,10 +212,10 @@ class _StrengthTestScreenState extends ConsumerState { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppTheme.primaryColor.withOpacity(0.1), + color: AppTheme.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all( - color: AppTheme.primaryColor.withOpacity(0.3)), + color: AppTheme.primaryColor.withValues(alpha: 0.3)), ), child: Row( children: [ @@ -281,7 +281,7 @@ class _ExerciseCard extends StatelessWidget { children: [ Text(title.toUpperCase(), style: const TextStyle( - color: Colors.grey, + color: AppTheme.textSecondary, fontSize: 12, fontWeight: FontWeight.bold)), const SizedBox(height: 8), @@ -290,7 +290,7 @@ class _ExerciseCard extends StatelessWidget { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: AppTheme.primaryColor.withOpacity(0.2), + color: AppTheme.primaryColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8)), child: Icon(icon, color: AppTheme.primaryColor), ), @@ -390,7 +390,7 @@ class _AdaptiveExerciseCard extends StatelessWidget { children: [ Text(slotTitle.toUpperCase(), style: const TextStyle( - color: Colors.grey, + color: AppTheme.textSecondary, fontSize: 12, fontWeight: FontWeight.bold)), Row( @@ -403,7 +403,7 @@ class _AdaptiveExerciseCard extends StatelessWidget { : Colors.grey)), Switch( value: isCapable, - activeColor: AppTheme.successColor, + activeThumbColor: AppTheme.successColor, onChanged: onToggleCapability, ), ], @@ -416,7 +416,7 @@ class _AdaptiveExerciseCard extends StatelessWidget { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: AppTheme.primaryColor.withOpacity(0.2), + color: AppTheme.primaryColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8)), child: Icon(icon, color: AppTheme.primaryColor), ), diff --git a/lib/src/features/onboarding/presentation/screens/welcome_screen.dart b/lib/src/features/onboarding/presentation/screens/welcome_screen.dart index e248da8..2bc912c 100644 --- a/lib/src/features/onboarding/presentation/screens/welcome_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/welcome_screen.dart @@ -20,7 +20,7 @@ class WelcomeScreen extends StatelessWidget { ), Positioned.fill( child: Container( - color: Colors.black.withOpacity(0.7), + color: Colors.black.withValues(alpha: 0.7), ), ), SafeArea( @@ -35,11 +35,11 @@ class WelcomeScreen extends StatelessWidget { width: 100, height: 100, decoration: BoxDecoration( - color: AppTheme.primaryColor.withOpacity(0.9), + color: AppTheme.primaryColor.withValues(alpha: 0.9), shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: AppTheme.primaryColor.withOpacity(0.5), + color: AppTheme.primaryColor.withValues(alpha: 0.5), blurRadius: 20) ], ), @@ -139,7 +139,7 @@ class _FeatureItem extends StatelessWidget { width: 48, height: 48, decoration: BoxDecoration( - color: AppTheme.primaryColor.withOpacity(0.2), + color: AppTheme.primaryColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(12), ), child: Icon( diff --git a/lib/src/features/stats/presentation/screens/stats_screen.dart b/lib/src/features/stats/presentation/screens/stats_screen.dart index d64f291..dce18a7 100644 --- a/lib/src/features/stats/presentation/screens/stats_screen.dart +++ b/lib/src/features/stats/presentation/screens/stats_screen.dart @@ -335,7 +335,7 @@ class _CurrentCycleCard extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: AppTheme.successColor.withOpacity(0.2), + color: AppTheme.successColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: const Text( @@ -502,14 +502,15 @@ class _FilterChip extends StatelessWidget { label: Text(label), selected: isSelected, onSelected: (_) => onTap(), - selectedColor: AppTheme.primaryColor.withOpacity(0.2), + selectedColor: AppTheme.primaryColor.withValues(alpha: 0.2), labelStyle: TextStyle( color: isSelected ? AppTheme.primaryColor : Colors.grey, fontWeight: FontWeight.bold, ), side: BorderSide( - color: - isSelected ? AppTheme.primaryColor : Colors.grey.withOpacity(0.3), + color: isSelected + ? AppTheme.primaryColor + : Colors.grey.withValues(alpha: 0.3), ), ); } diff --git a/lib/src/features/stats/presentation/widgets/progress_chart.dart b/lib/src/features/stats/presentation/widgets/progress_chart.dart index 223124d..4f1d300 100644 --- a/lib/src/features/stats/presentation/widgets/progress_chart.dart +++ b/lib/src/features/stats/presentation/widgets/progress_chart.dart @@ -51,7 +51,7 @@ class ProgressChart extends StatelessWidget { decoration: BoxDecoration( color: AppTheme.surfaceColor, borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppTheme.primaryColor.withOpacity(0.1)), + border: Border.all(color: AppTheme.primaryColor.withValues(alpha: 0.1)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -148,14 +148,12 @@ class ProgressChart extends StatelessWidget { ), belowBarData: BarAreaData( show: true, - color: AppTheme.primaryColor.withOpacity(0.1), + color: AppTheme.primaryColor.withValues(alpha: 0.1), ), ), ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( - // FIX 2: Alte API nutzen (tooltipBgColor statt getTooltipColor) - // tooltipBgColor: AppTheme.surfaceColor, getTooltipColor: (touchedSpot) => AppTheme.surfaceColor, getTooltipItems: (touchedSpots) { return touchedSpots.map((spot) { diff --git a/lib/src/features/workout_runner/application/workout_generator_service.dart b/lib/src/features/workout_runner/application/workout_generator_service.dart new file mode 100644 index 0000000..c35e558 --- /dev/null +++ b/lib/src/features/workout_runner/application/workout_generator_service.dart @@ -0,0 +1,256 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../shared/data/local/app_database.dart'; +import '../../../shared/domain/entities/exercise.dart'; +import '../../../shared/domain/entities/workout_set.dart'; +import '../../../shared/domain/logic/wendler_calculator.dart'; + +final workoutGeneratorServiceProvider = + Provider((ref) { + return WorkoutGeneratorService(); +}); + +class WorkoutGeneratorService { + List generateWorkout({ + required int week, + required int day, + required Map trainingMaxes, + required UserCollection user, + required AccessoryTemplate template, + int? conditioningSets, + }) { + final exercises = []; + + exercises.addAll(_generateMainLifts(week, day, trainingMaxes, user)); + + if (template == AccessoryTemplate.hypertrophy) { + exercises + .addAll(_generateHypertrophyAccessories(day, trainingMaxes, user)); + } else if (template == AccessoryTemplate.conditioning) { + final sets = (conditioningSets != null && conditioningSets > 0) + ? conditioningSets + : 15; + exercises.addAll(_generateConditioning(day, sets)); + } + + return exercises; + } + + List _generateMainLifts(int week, int day, + Map trainingMaxes, UserCollection user) { + final exercises = []; + final variants = user.exerciseVariants ?? {}; + + (String, String, ExerciseType) resolveVariant(String slot, String defaultId, + String defaultName, ExerciseType defaultType) { + final variant = variants[slot]; + if (slot == 'pull') { + if (variant == 'row') return ('row', 'Pendlay Row', ExerciseType.row); + return ('pullup', 'Weighted Pull-up', ExerciseType.pullup); + } + if (slot == 'push') { + if (variant == 'bench') { + return ('bench', 'Bench Press', ExerciseType.bench); + } + return ('dip', 'Weighted Dip', ExerciseType.dip); + } + return (defaultId, defaultName, defaultType); + } + + void addExercise(String slot, String defaultId, String defaultName, + ExerciseType defaultType, bool isMain) { + final (id, name, type) = + resolveVariant(slot, defaultId, defaultName, defaultType); + + final tm = trainingMaxes[defaultId] ?? 0.0; + 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, + ); + } + } else { + if (week == 4) return; + if (type == ExerciseType.row || type == ExerciseType.bench) return; + + sets = WendlerCalculator.generateFSLSets( + trainingMax: tm, + exerciseType: type, + currentBodyweight: user.currentBodyweight, + ); + } + + if (sets.isNotEmpty) { + exercises.add(Exercise( + exerciseId: id, + exerciseName: isMain ? name : '$name (FSL)', + bodyweightAtSession: user.currentBodyweight, + sets: sets, + )); + } + } + + if (day == 1) { + addExercise('legs', 'squat', 'Back Squat', ExerciseType.squat, true); + addExercise( + 'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, false); + } else if (day == 2) { + addExercise('push', 'dip', 'Weighted Dip', ExerciseType.dip, true); + addExercise('legs', 'squat', 'Back Squat', ExerciseType.squat, false); + } else if (day == 3) { + addExercise( + 'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, true); + addExercise('push', 'dip', 'Weighted Dip', ExerciseType.dip, false); + } + + return exercises; + } + + List _generateHypertrophyAccessories( + int day, Map trainingMaxes, UserCollection user) { + final accessories = []; + + double calculateWeight(double referenceTm, double percentage) { + final raw = referenceTm * percentage; + return (raw / 2.5).round() * 2.5; + } + + Exercise createSimple(String id, String name, int sets, int reps, + {double weight = 0.0}) { + return Exercise( + exerciseId: id, + exerciseName: name, + bodyweightAtSession: 0, + sets: List.generate( + sets, + (i) => WorkoutSet( + setNumber: i + 1, + repsTarget: reps, + targetWeightTotal: weight, + repsActual: 0, + isAmrap: false, + completed: false, + )), + ); + } + + final squatTm = trainingMaxes['squat'] ?? 0.0; + final dipTm = trainingMaxes['dip'] ?? 0.0; + final pullupTm = trainingMaxes['pullup'] ?? 0.0; + + switch (day) { + case 1: // Squat Tag + // RDL: ~40% vom Squat TM + accessories.add(createSimple('rdl', 'Romanian Deadlift', 3, 10, + weight: calculateWeight(squatTm, 0.4))); + + accessories.add(_createIntervalExercise( + id: 'kb_swing', + name: '2H KB Swing', + sets: 10, + intervalSeconds: 60, + repsPerSet: 10)); + break; + + case 2: // Dip Tag (Push) + // OHP: ~30% vom System-Dip-TM (konservativ für 3x10) + accessories.add(createSimple('ohp', 'Overhead Press', 3, 10, + weight: calculateWeight(dipTm, 0.3))); + + accessories.add(createSimple('face_pull', 'Band Face Pull', 3, 10)); + accessories.add(createSimple('ab_roll', 'Ab Wheel Rollout', 3, 10)); + break; + + case 3: // Pullup Tag (Pull) + // Curls: ~20% vom System-Pullup-TM + accessories.add(createSimple('curl', 'Barbell Curl', 3, 10, + weight: calculateWeight(pullupTm, 0.2))); + + accessories.add(_createIntervalExercise( + id: 'kb_snatch_acc', + name: 'KB Snatch', + sets: 10, + intervalSeconds: 60, + repsPerSet: 5)); + accessories.add(createSimple('plank', 'Plank (30s)', 3, 1)); + break; + } + return accessories; + } + + List _generateConditioning(int day, int targetSets) { + final accessories = []; + + const totalTimeSeconds = 20 * 60; + final intervalSeconds = (totalTimeSeconds / targetSets).floor(); + + String id; + String name; + + switch (day) { + case 1: + id = 'kb_clean_press'; + name = 'KB Clean & Press'; + break; + case 2: + id = 'kb_snatch_cond'; + name = 'KB Snatch'; + break; + case 3: + id = 'kb_thruster'; + name = 'KB Thruster'; + break; + default: + return []; + } + + accessories.add(_createIntervalExercise( + id: id, + name: name, + sets: targetSets, + intervalSeconds: intervalSeconds, + repsPerSet: 5, + )); + + return accessories; + } + + Exercise _createIntervalExercise({ + required String id, + required String name, + required int sets, + required int intervalSeconds, + required int repsPerSet, + }) { + return Exercise( + exerciseId: id, + exerciseName: '$name (${_formatIntervalName(intervalSeconds)})', + bodyweightAtSession: 0, + intervalSeconds: intervalSeconds, + sets: List.generate( + sets, + (i) => WorkoutSet( + setNumber: i + 1, + repsTarget: repsPerSet, + targetWeightTotal: 0, + repsActual: 0, + isAmrap: false, + completed: false, + )), + ); + } + + String _formatIntervalName(int seconds) { + if (seconds == 60) return 'EMOM'; + return 'E${seconds}S'; + } +} 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 360a083..e085327 100644 --- a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart +++ b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart @@ -16,9 +16,9 @@ import '../../../../shared/data/repositories/cycle_repository.dart'; import '../../../../shared/data/repositories/workout_repository.dart'; import '../../../../shared/data/remote/sync_service.dart'; import '../widgets/plate_visualizer.dart'; -import '../widgets/timer_widget.dart'; import '../widgets/enemy_hp_bar.dart'; import '../../../gamification/application/quest_service.dart'; +import '../widgets/emom_timer_widget.dart'; class BattleScreen extends ConsumerStatefulWidget { final int week; @@ -73,6 +73,37 @@ class _BattleScreenState extends ConsumerState { } } + void _handleEmomSetComplete() { + final currentExercise = _exercises[_currentExerciseIndex]; + final currentSet = currentExercise.sets[_currentSetIndex]; + + final updatedSet = currentSet.copyWith( + repsActual: currentSet.repsTarget, + completed: true, + ); + + final updatedSets = List.from(currentExercise.sets); + updatedSets[_currentSetIndex] = updatedSet; + + final updatedExercise = currentExercise.copyWith(sets: updatedSets); + final updatedExercises = List.from(_exercises); + updatedExercises[_currentExerciseIndex] = updatedExercise; + + if (_currentSetIndex < currentExercise.sets.length - 1) { + setState(() { + _exercises = updatedExercises; + _currentSetIndex++; + + _repsCompleted = currentExercise.sets[_currentSetIndex].repsTarget; + }); + } else { + setState(() { + _exercises = updatedExercises; + }); + _showEmomFinishDialog(); + } + } + List> _getExerciseConfig(int day, UserCollection user) { final variants = user.exerciseVariants ?? {}; @@ -146,56 +177,77 @@ class _BattleScreenState extends ConsumerState { Future _loadWorkout() async { final userRepo = ref.read(userRepositoryProvider); + final workoutRepo = ref.read(workoutRepositoryProvider); final cycleRepo = ref.read(cycleRepositoryProvider); final user = await userRepo.getLocalUser(); - final trainingMaxesMap = await cycleRepo.getCurrentTrainingMaxesAsync(); if (user == null) { if (mounted) context.go('/hub'); return; } - final exercises = []; - final exerciseConfigs = _getExerciseConfig(widget.day, user); + List exercises = []; - for (final config in exerciseConfigs) { - final id = config['id'] as String; - final name = config['name'] as String; - final type = config['type'] as ExerciseType; - final isMain = config['isMain'] as bool; + if (widget.workoutId != null) { + try { + final allWorkouts = await workoutRepo.getAllWorkouts(); - String tmKey = id; - if (id == 'bench') tmKey = 'dip'; - if (id == 'row') tmKey = 'pullup'; + final loadedWorkout = + allWorkouts.where((w) => w.id == widget.workoutId).firstOrNull; - final tm = trainingMaxesMap[tmKey] ?? 0.0; - List sets = []; + if (loadedWorkout != null && loadedWorkout.exercises.isNotEmpty) { + exercises = loadedWorkout.exercises.map((e) { + return Exercise.fromJson(e as Map); + }).toList(); + } + } catch (e) { + debugPrint('⚠️ Fehler beim Laden des gespeicherten Workouts: $e'); + } + } - if (isMain) { - sets = WendlerCalculator.generateSets( - week: widget.week, - trainingMax: tm, - exerciseType: type, - currentBodyweight: user.currentBodyweight, - ); - } else { - if (widget.week != 4) { - sets = WendlerCalculator.generateFSLSets( + if (exercises.isEmpty) { + final trainingMaxesMap = await cycleRepo.getCurrentTrainingMaxesAsync(); + final exerciseConfigs = _getExerciseConfig(widget.day, user); + + for (final config in exerciseConfigs) { + final id = config['id'] as String; + final name = config['name'] as String; + final type = config['type'] as ExerciseType; + final isMain = config['isMain'] as bool; + + String tmKey = id; + if (id == 'bench') tmKey = 'dip'; + if (id == 'row') tmKey = 'pullup'; + + final tm = trainingMaxesMap[tmKey] ?? 0.0; + List sets = []; + + if (isMain) { + sets = WendlerCalculator.generateSets( + week: widget.week, trainingMax: tm, exerciseType: type, currentBodyweight: user.currentBodyweight, ); + } else { + if (widget.week != 4) { + sets = WendlerCalculator.generateFSLSets( + trainingMax: tm, + exerciseType: type, + currentBodyweight: user.currentBodyweight, + ); + } } - } - if (sets.isNotEmpty) { - exercises.add(Exercise( - exerciseId: id, - exerciseName: isMain ? name : '$name (FSL)', - bodyweightAtSession: user.currentBodyweight, - sets: sets, - )); + if (sets.isNotEmpty) { + exercises.add(Exercise( + exerciseId: id, + exerciseName: isMain ? name : '$name (FSL)', + bodyweightAtSession: user.currentBodyweight, + sets: sets, + )); + } } } @@ -458,9 +510,10 @@ class _BattleScreenState extends ConsumerState { return FutureBuilder>( future: ref.read(userRepositoryProvider).getInventorySettingsAsync(), builder: (context, snapshot) { - if (!snapshot.hasData) + if (!snapshot.hasData) { return const Scaffold( body: Center(child: CircularProgressIndicator())); + } final inventory = snapshot.data!; @@ -482,7 +535,10 @@ class _BattleScreenState extends ConsumerState { final isTwoSided = currentExercise.exerciseId == 'squat' || currentExercise.exerciseId == 'row' || - currentExercise.exerciseId == 'bench'; + currentExercise.exerciseId == 'bench' || + currentExercise.exerciseId == 'rdl' || + currentExercise.exerciseId == 'ohp' || + currentExercise.exerciseId == 'curl'; final isBodyweight = !isTwoSided; final barWeight = isBodyweight ? currentExercise.bodyweightAtSession @@ -553,14 +609,16 @@ class _BattleScreenState extends ConsumerState { ), Positioned.fill( child: Container( - color: Colors.black.withOpacity(0.7), + color: Colors.black.withValues(alpha: 0.7), ), ), - SafeArea( - child: _isResting - ? _buildRestScreen() - : _buildWorkoutScreen(currentExercise, currentSet, - plateResult, completedHP, totalHP), + Positioned.fill( + child: SafeArea( + child: _isResting + ? _buildRestScreen() + : _buildWorkoutScreen(currentExercise, currentSet, + plateResult, completedHP, totalHP), + ), ), ], ), @@ -633,6 +691,10 @@ class _BattleScreenState extends ConsumerState { int completedHP, int totalHP, ) { + if (currentExercise.intervalSeconds != null && + currentExercise.intervalSeconds! > 0) { + return _buildEmomView(currentExercise, currentSet, completedHP, totalHP); + } final readableStyle = Theme.of(context).textTheme.bodyLarge?.copyWith( color: Colors.white, shadows: [ @@ -660,7 +722,7 @@ class _BattleScreenState extends ConsumerState { child: Image.asset( _getEnemyAsset(currentExercise.exerciseId), fit: BoxFit.contain, - color: Colors.white.withOpacity(0.9), + color: Colors.white.withValues(alpha: 0.9), colorBlendMode: BlendMode.modulate, ), ), @@ -707,12 +769,12 @@ class _BattleScreenState extends ConsumerState { flex: 6, child: Container( decoration: BoxDecoration( - color: AppTheme.surfaceColor.withOpacity(0.95), + color: AppTheme.surfaceColor.withValues(alpha: 0.95), borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.5), + color: Colors.black.withValues(alpha: 0.5), blurRadius: 20, offset: const Offset(0, -5)) ], @@ -760,7 +822,8 @@ class _BattleScreenState extends ConsumerState { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: AppTheme.primaryColor.withOpacity(0.1), + color: + AppTheme.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.primaryColor), ), @@ -925,6 +988,263 @@ class _BattleScreenState extends ConsumerState { }, ); } + + Widget _buildEmomView( + Exercise currentExercise, + WorkoutSet currentSet, + int completedHP, + int totalHP, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.surfaceColor.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white10), + ), + child: Row( + children: [ + SizedBox( + height: 60, + width: 60, + child: Image.asset( + _getEnemyAsset(currentExercise.exerciseId), + fit: BoxFit.contain, + errorBuilder: (c, o, s) => const Icon(Icons.fitness_center, + size: 40, color: Colors.white), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentExercise.exerciseName, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.white), + ), + Text( + '${currentSet.repsTarget} Reps per Round', + style: const TextStyle(color: Colors.grey), + ), + ], + ), + ), + SizedBox( + width: 80, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${totalHP - completedHP}/$totalHP HP', + style: const TextStyle( + color: AppTheme.errorColor, + fontWeight: FontWeight.bold, + fontSize: 10), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: totalHP > 0 + ? (totalHP - completedHP) / totalHP + : 0.0, + backgroundColor: Colors.red[900], + color: AppTheme.errorColor, + minHeight: 6, + ), + ), + ], + ), + ), + ], + ), + ), + Expanded( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + EmomTimerWidget( + key: ValueKey( + '${currentExercise.exerciseId}_$_currentExerciseIndex'), + intervalSeconds: currentExercise.intervalSeconds!, + totalSets: currentExercise.sets.length, + currentSet: _currentSetIndex + 1, + onSetComplete: _handleEmomSetComplete, + onWorkoutComplete: _handleEmomSetComplete, + ), + const SizedBox(height: 32), + if (currentSet.targetWeightTotal > 0) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + decoration: BoxDecoration( + border: Border.all(color: AppTheme.primaryColor), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'WEIGHT: ${currentSet.targetWeightTotal} kg', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } + + void _adjustEmomSets(int newTotalSets) { + final currentEx = _exercises[_currentExerciseIndex]; + + if (newTotalSets == currentEx.sets.length) return; + + List currentSets = List.from(currentEx.sets); + + if (newTotalSets > currentSets.length) { + final templateSet = currentSets.last; + + for (int i = currentSets.length; i < newTotalSets; i++) { + currentSets.add(templateSet.copyWith( + setNumber: i + 1, + completed: true, + repsActual: templateSet.repsTarget, + )); + } + } else { + currentSets = currentSets.sublist(0, newTotalSets); + } + + final updatedEx = currentEx.copyWith(sets: currentSets); + final updatedExercises = List.from(_exercises); + updatedExercises[_currentExerciseIndex] = updatedEx; + + setState(() { + _exercises = updatedExercises; + + _currentSetIndex = newTotalSets - 1; + + _repsCompleted = updatedEx.sets.last.repsTarget; + }); + } + + void _showEmomFinishDialog() { + final currentEx = _exercises[_currentExerciseIndex]; + int setsCount = currentEx.sets.length; + + showModalBottomSheet( + context: context, + backgroundColor: AppTheme.surfaceColor, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.timer_off, + size: 48, color: AppTheme.primaryColor), + const SizedBox(height: 16), + Text( + 'MISSION ACCOMPLISHED', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + ), + const SizedBox(height: 8), + const Text( + 'Time is up. Did you push further?', + style: TextStyle(color: Colors.grey), + ), + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _CounterButton( + icon: Icons.remove, + onTap: setsCount > 1 + ? () => setModalState(() => setsCount--) + : null, + ), + Container( + width: 140, + alignment: Alignment.center, + child: Column( + children: [ + Text( + '$setsCount', + style: const TextStyle( + fontSize: 64, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const Text('SETS COMPLETED', + style: TextStyle( + color: AppTheme.primaryColor, + fontSize: 10, + fontWeight: FontWeight.bold)), + ], + ), + ), + _CounterButton( + icon: Icons.add, + onTap: () => setModalState(() => setsCount++), + ), + ], + ), + const SizedBox(height: 32), + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + + _adjustEmomSets(setsCount); + + _completeSet(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.successColor, + foregroundColor: Colors.white, + ), + child: const Text('CONFIRM & FINISH', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(height: 16), + ], + ), + ); + }, + ); + }, + ); + } } class _InfoBox extends StatelessWidget { @@ -968,7 +1288,7 @@ class _CounterButton extends StatelessWidget { decoration: BoxDecoration( color: onTap != null ? AppTheme.primaryColor - : Colors.grey.withOpacity(0.1), + : Colors.grey.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon(icon, diff --git a/lib/src/features/workout_runner/presentation/screens/battle_screen_back b/lib/src/features/workout_runner/presentation/screens/battle_screen_back deleted file mode 100644 index 25abe80..0000000 --- a/lib/src/features/workout_runner/presentation/screens/battle_screen_back +++ /dev/null @@ -1,1011 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'dart:async'; -import 'dart:convert'; - -import '../../../../core/constants/asset_paths.dart'; -import '../../../../core/theme/app_theme.dart'; -import '../../../../shared/domain/entities/exercise.dart'; -import '../../../../shared/domain/entities/workout_set.dart'; -import '../../../../shared/domain/logic/wendler_calculator.dart'; -import '../../../../shared/domain/logic/plate_calculator.dart'; -import '../../../../shared/domain/logic/xp_calculator.dart'; -import '../../../../shared/data/repositories/user_repository.dart'; -import '../../../../shared/data/repositories/cycle_repository.dart'; -import '../../../../shared/data/repositories/workout_repository.dart'; -import '../../../../shared/data/remote/sync_service.dart'; -import '../widgets/plate_visualizer.dart'; -import '../widgets/timer_widget.dart'; -import '../widgets/enemy_hp_bar.dart'; - -class BattleScreen extends ConsumerStatefulWidget { - final int week; - final int day; - final int? workoutId; - - const BattleScreen({ - super.key, - required this.week, - required this.day, - this.workoutId, - }); - - @override - ConsumerState createState() => _BattleScreenState(); -} - -class _BattleScreenState extends ConsumerState { - List _exercises = []; - int _currentExerciseIndex = 0; - int _currentSetIndex = 0; - int _repsCompleted = 0; - bool _isLoading = true; - Timer? _restTimer; - int _restSeconds = 0; - bool _isResting = false; - - @override - void initState() { - super.initState(); - _loadWorkout(); - } - - @override - void dispose() { - _restTimer?.cancel(); - super.dispose(); - } - - String _getEnemyAsset(String exerciseId) { - // Mapping basierend auf Übungs-ID - switch (exerciseId) { - case 'squat': - return AssetPaths.enemyIronGolem; - case 'pullup': - return AssetPaths.enemyGravityDemon; - case 'dip': - return AssetPaths.enemyPressurePhantom; - default: - return AssetPaths.enemyIronGolem; // Fallback - } - } - - List> _getExerciseConfig(int day) { - switch (day) { - case 1: - return [ - { - 'id': 'squat', - 'name': 'Back Squat', - 'type': ExerciseType.squat, - 'isMain': true - }, - { - 'id': 'pullup', - 'name': 'Weighted Pull-up', - 'type': ExerciseType.pullup, - 'isMain': false - }, - ]; - case 2: - return [ - { - 'id': 'dip', - 'name': 'Weighted Dip', - 'type': ExerciseType.dip, - 'isMain': true - }, - { - 'id': 'squat', - 'name': 'Back Squat', - 'type': ExerciseType.squat, - 'isMain': false - }, - ]; - case 3: - return [ - { - 'id': 'pullup', - 'name': 'Weighted Pull-up', - 'type': ExerciseType.pullup, - 'isMain': true - }, - { - 'id': 'dip', - 'name': 'Weighted Dip', - 'type': ExerciseType.dip, - 'isMain': false - }, - ]; - default: - return []; - } - } - - Future _loadWorkout() async { - final userRepo = ref.read(userRepositoryProvider); - final cycleRepo = ref.read(cycleRepositoryProvider); - - final user = await userRepo.getLocalUser(); - final cycle = await cycleRepo.getCurrentCycle(); - - if (user == null || cycle == null) { - if (mounted) context.go('/hub'); - return; - } - - final trainingMaxes = cycleRepo.getCurrentTrainingMaxes(); - final exercises = []; - - final exerciseConfigs = _getExerciseConfig(widget.day); - - for (final config in exerciseConfigs) { - final id = config['id'] as String; - final name = config['name'] as String; - final type = config['type'] as ExerciseType; - final isMain = config['isMain'] as bool; - - final tm = trainingMaxes[id] ?? 0.0; - List sets = []; - - if (isMain) { - sets = WendlerCalculator.generateSets( - week: widget.week, - trainingMax: tm, - exerciseType: type, - currentBodyweight: user.currentBodyweight, - ); - } else { - if (widget.week != 4) { - sets = WendlerCalculator.generateFSLSets( - trainingMax: tm, - exerciseType: type, - currentBodyweight: user.currentBodyweight, - ); - } - } - - if (sets.isNotEmpty) { - exercises.add(Exercise( - exerciseId: id, - exerciseName: isMain ? name : '$name (FSL)', - bodyweightAtSession: user.currentBodyweight, - sets: sets, - )); - } - } - - setState(() { - _exercises = exercises; - _isLoading = false; - - if (exercises.isNotEmpty && exercises.first.sets.isNotEmpty) { - _repsCompleted = exercises.first.sets.first.repsTarget; - } - }); - } - - void _completeSet() { - final currentExercise = _exercises[_currentExerciseIndex]; - final currentSet = currentExercise.sets[_currentSetIndex]; - - final updatedSet = currentSet.copyWith( - repsActual: _repsCompleted, - completed: true, - ); - - final updatedSets = List.from(currentExercise.sets); - updatedSets[_currentSetIndex] = updatedSet; - - final updatedExercise = currentExercise.copyWith(sets: updatedSets); - final updatedExercises = List.from(_exercises); - updatedExercises[_currentExerciseIndex] = updatedExercise; - - int nextRepsTarget = 0; - - if (_currentSetIndex < currentExercise.sets.length - 1) { - nextRepsTarget = currentExercise.sets[_currentSetIndex + 1].repsTarget; - - setState(() { - _exercises = updatedExercises; - _currentSetIndex++; - _repsCompleted = nextRepsTarget; - }); - _startRestTimer(90); - } else if (_currentExerciseIndex < _exercises.length - 1) { - final nextExercise = _exercises[_currentExerciseIndex + 1]; - if (nextExercise.sets.isNotEmpty) { - nextRepsTarget = nextExercise.sets.first.repsTarget; - } - - setState(() { - _exercises = updatedExercises; - _currentExerciseIndex++; - _currentSetIndex = 0; - _repsCompleted = nextRepsTarget; - }); - _startRestTimer(180); - } else { - setState(() { - _exercises = updatedExercises; - }); - _completeWorkout(); - } - } - - void _startRestTimer(int seconds) { - setState(() { - _isResting = true; - _restSeconds = seconds; - }); - - _restTimer?.cancel(); - _restTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (_restSeconds > 0) { - setState(() => _restSeconds--); - } else { - timer.cancel(); - setState(() => _isResting = false); - } - }); - } - - void _skipRest() { - _restTimer?.cancel(); - setState(() { - _isResting = false; - _restSeconds = 0; - }); - } - - Future _completeWorkout() async { - final xpEarned = XPCalculator.calculateWorkoutXP(_exercises); - - final userRepo = ref.read(userRepositoryProvider); - await userRepo.updateXP(xpEarned); - - final user = await userRepo.getLocalUser(); - if (user != null) { - final newLevel = XPCalculator.calculateLevelFromXP(user.xp); - if (newLevel > user.level) { - await userRepo.updateLevel(newLevel); - if (mounted) { - _showLevelUpDialog(user.level, newLevel); - } - } - } - - if (widget.workoutId != null) { - final workoutRepo = ref.read(workoutRepositoryProvider); - final cycleRepo = ref.read(cycleRepositoryProvider); - final cycle = await cycleRepo.getCurrentCycle(); - - final cycleIdRef = cycle?.serverId ?? cycle?.id.toString() ?? ''; - - var workout = await workoutRepo.getWorkoutByWeekDay( - cycleId: cycleIdRef, week: widget.week, day: widget.day); - - if (workout != null) { - workout.exercisesJson = - jsonEncode(_exercises.map((e) => e.toJson()).toList()); - await workoutRepo.completeWorkout(workout, xpEarned: xpEarned); - - ref.read(syncServiceProvider).sync(); - } - } - - if (mounted) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AlertDialog( - title: const Text('RAID COMPLETE!'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.emoji_events, - size: 64, - color: AppTheme.primaryColor, - ), - const SizedBox(height: 16), - Text( - '+$xpEarned XP', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: AppTheme.primaryColor, - ), - ), - ], - ), - actions: [ - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - context.go('/hub'); - }, - child: const Text('BACK TO HUB'), - ), - ], - ), - ); - } - } - - void _showLevelUpDialog(int oldLevel, int newLevel) { - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: AppTheme.primaryColor, - title: const Text( - 'LEVEL UP!', - style: TextStyle(color: Colors.black), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.military_tech, size: 80, color: Colors.black), - const SizedBox(height: 16), - Text( - '$oldLevel → $newLevel', - style: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: - const Text('CONTINUE', style: TextStyle(color: Colors.black)), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - if (_isLoading) { - return const Scaffold(body: Center(child: CircularProgressIndicator())); - } - - if (_exercises.isEmpty) { - return Scaffold( - appBar: AppBar(title: const Text('Battle')), - body: const Center(child: Text('No exercises configured')), - ); - } - - final currentExercise = _exercises[_currentExerciseIndex]; - final currentSet = currentExercise.sets[_currentSetIndex]; - final userRepo = ref.watch(userRepositoryProvider); - - final totalHP = _exercises.fold( - 0, - (sum, ex) => sum + ex.sets.fold(0, (s, set) => s + set.repsTarget), - ); - - final completedHP = _exercises.take(_currentExerciseIndex).fold( - 0, - (sum, ex) => - sum + ex.sets.fold(0, (s, set) => s + set.repsActual), - ) + - currentExercise.sets - .take(_currentSetIndex) - .fold(0, (sum, set) => sum + set.repsActual); - - final isBodyweight = currentExercise.exerciseId != 'squat'; - final barWeight = isBodyweight - ? currentExercise.bodyweightAtSession - : userRepo.getBarWeight(); - final availablePlates = userRepo.getAvailablePlates(); - final inventory = userRepo.getInventorySettings(); - final bandsList = - (inventory['bands'] as List?)?.cast>() ?? []; - - final Map availableBands = {}; - for (var band in bandsList) { - final color = band['color'] as String; - final resistance = (band['resistance_kg'] as num).toDouble(); - if (band['count'] as int > 0) { - availableBands[color] = resistance; - } - } - - final plateResult = PlateCalculator.calculate( - targetWeight: currentSet.targetWeightTotal, - barWeight: barWeight, - availablePlates: availablePlates, - availableBands: availableBands, - isTwoSided: !isBodyweight, - ); - - return Scaffold( - appBar: AppBar( - title: Text('Week ${widget.week} - Day ${widget.day}'), - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Abandon Raid?'), - content: const Text('Your progress will not be saved.'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('CANCEL'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - context.go('/hub'); - }, - style: TextButton.styleFrom( - foregroundColor: AppTheme.errorColor), - child: const Text('ABANDON'), - ), - ], - ), - ); - }, - ), - ), - body: Stack( - children: [ - // 1. HINTERGRUND (Underground Gym) - Positioned.fill( - child: Image.asset( - AssetPaths.bgUndergroundGym, - fit: BoxFit.cover, - ), - ), - - // 2. Overlay (Atmosphäre & Lesbarkeit) - Positioned.fill( - child: Container( - color: Colors.black.withOpacity(0.7), // Dunkler Schleier - ), - ), - - // 3. INHALT - SafeArea( - child: _isResting - ? _buildRestScreen() // Rest Screen überdeckt das Gym (oder man macht ihn auch transparent) - : _buildWorkoutScreen(currentExercise, currentSet, plateResult, - completedHP, totalHP), - ), - ], - ), - // body: _isResting - // ? _buildRestScreen() - // : _buildWorkoutScreen( - // currentExercise, currentSet, plateResult, completedHP, totalHP), - ); - } - - Widget _buildRestScreen() { - return Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - AppTheme.backgroundColor, - AppTheme.surfaceColor, - ], - ), - ), - 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, - ), - ), - ], - ), - ), - const SizedBox(height: 48), - ElevatedButton( - onPressed: _skipRest, - child: const Text('SKIP REST'), - ), - ], - ), - ), - ); - } - - Widget _buildWorkoutScreen( - Exercise currentExercise, - WorkoutSet currentSet, - PlateLoadResult plateResult, - int completedHP, - int totalHP, - ) { - // Gemeinsamer Text-Style für bessere Lesbarkeit auf Hintergründen - final readableStyle = Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Colors.white, - shadows: [ - const Shadow(color: Colors.black, blurRadius: 4, offset: Offset(0, 1)) - ], - ); - - final titleStyle = Theme.of(context).textTheme.titleLarge?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - shadows: [ - const Shadow(color: Colors.black, blurRadius: 8, offset: Offset(0, 2)) - ], - ); - - return Column( - children: [ - // Info Header (HP & Wave) - Container( - padding: const EdgeInsets.all(16), - margin: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.surfaceColor.withOpacity(0.9), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white10), - ), - child: Column( - children: [ - // --- NEU: Enemy Image --- - SizedBox( - height: 120, // Gute Größe für den Header - child: Image.asset( - _getEnemyAsset( - currentExercise.exerciseId), // Wählt das richtige Bild - fit: BoxFit.contain, - // Ein leichter Schatten, damit der Gegner nicht "schwebt" - color: Colors.black.withOpacity(0.2), - colorBlendMode: BlendMode.dstOver, - // Fallback Icon, falls Asset fehlt - errorBuilder: (c, o, s) => const Icon( - Icons.warning_amber_rounded, - size: 48, - color: Colors.white24), - ), - ), - const SizedBox(height: 16), // Abstand zum Text - // ------------------------ - Text( - 'Wave ${_currentExerciseIndex + 1}/${_exercises.length}', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - EnemyHPBar( - current: totalHP - completedHP, - max: totalHP, - ), - ], - ), - ), - - // Scrollbarer Inhalt (nimmt den verfügbaren Platz ein) - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - currentExercise.exerciseName, - style: Theme.of(context).textTheme.displayMedium?.copyWith( - shadows: [ - const Shadow(color: Colors.black, blurRadius: 10) - ], - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - 'Set ${_currentSetIndex + 1}/${currentExercise.sets.length}', - style: titleStyle, // Neuer Style - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - - // Target Card (etwas transparenter für Look) - Card( - color: AppTheme.surfaceColor.withOpacity(0.95), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Text('TARGET', - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith(color: AppTheme.textSecondary)), - const SizedBox(height: 8), - Text( - '${currentSet.targetWeightTotal.toStringAsFixed(1)} kg', - style: Theme.of(context) - .textTheme - .displayMedium - ?.copyWith(color: AppTheme.primaryColor), - ), - Text( - '${currentSet.targetPercentage}% TM × ${currentSet.repsTarget} reps${currentSet.isAmrap ? '+' : ''}', - style: readableStyle, // Neuer Style - ), - ], - ), - ), - ), - const SizedBox(height: 24), - - // Visualizer oder Band Info - // Wir geben dem Container eine feste Mindesthöhe, damit das Layout weniger springt, - // aber da der Button jetzt fixiert ist, ist das Springen weniger kritisch. - if (plateResult.bandAssistance != null) - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.primaryColor - .withOpacity(0.2), // Heller für Kontrast - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppTheme.primaryColor), - ), - child: Column( - children: [ - const Icon(Icons.help_outline, - size: 48, color: AppTheme.primaryColor), - const SizedBox(height: 8), - Text( - 'ASSISTANCE NEEDED', - style: titleStyle?.copyWith( - color: AppTheme.primaryColor), - ), - const SizedBox(height: 8), - Text( - 'Use ${plateResult.bandAssistance} Band', - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - Text( - '(approx. -${(plateResult.totalAchieved - currentSet.targetWeightTotal).abs().toStringAsFixed(1)} kg)', - style: readableStyle, // Neuer Style - ), - ], - ), - ) - else - PlateVisualizer( - plateConfiguration: plateResult.plateConfiguration, - isTwoSided: currentExercise.exerciseId == 'squat', - exerciseName: currentExercise.exerciseName, - ), - - const SizedBox(height: 32), - - Text( - 'REPS COMPLETED', - style: titleStyle, - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - - // Counter (Hintergrund hinzufügen für Lesbarkeit) - Container( - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(30), - ), - padding: - const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.remove_circle), - iconSize: 48, - color: AppTheme.primaryColor, - onPressed: _repsCompleted > 0 - ? () => setState(() => _repsCompleted--) - : null, - ), - const SizedBox(width: 24), - SizedBox( - width: 80, - child: Text( - _repsCompleted.toString(), - style: Theme.of(context) - .textTheme - .displayLarge - ?.copyWith(fontSize: 56, color: Colors.white), - textAlign: TextAlign.center, - ), - ), - const SizedBox(width: 24), - IconButton( - icon: const Icon(Icons.add_circle), - iconSize: 48, - color: AppTheme.primaryColor, - onPressed: () => setState(() => _repsCompleted++), - ), - ], - ), - ), - - if (currentSet.isAmrap) - Padding( - padding: const EdgeInsets.only(top: 12), - child: Text( - '🔥 AMRAP - Go for max reps! 🔥', - style: readableStyle?.copyWith( - color: AppTheme.secondaryColor, - fontWeight: FontWeight.bold, - fontSize: 18), - textAlign: TextAlign.center, - ), - ), - - // Platzhalter am Ende, damit man nicht "hinter" den fixierten Button scrollen muss - const SizedBox(height: 100), - ], - ), - ), - ), - - // --- FIXIERTER BUTTON BEREICH --- - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.surfaceColor, // Solider Hintergrund - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.5), - blurRadius: 10, - offset: const Offset(0, -5), - ), - ], - ), - child: SafeArea( - top: false, - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _repsCompleted >= currentSet.repsTarget - ? _completeSet - : null, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - backgroundColor: AppTheme.primaryColor, - foregroundColor: Colors.black, - ), - child: const Text( - 'COMPLETE SET', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - letterSpacing: 1.5), - ), - ), - ), - ), - ), - ], - ); - } - // Widget _buildWorkoutScreen( - // Exercise currentExercise, - // WorkoutSet currentSet, - // PlateLoadResult plateResult, - // int completedHP, - // int totalHP, - // ) { - // return Column( - // children: [ - // Container( - // padding: const EdgeInsets.all(16), - // color: AppTheme.surfaceColor, - // child: Column( - // children: [ - // Text( - // 'Wave ${_currentExerciseIndex + 1}/${_exercises.length}', - // style: Theme.of(context).textTheme.titleMedium, - // ), - // const SizedBox(height: 8), - // EnemyHPBar( - // current: totalHP - completedHP, - // max: totalHP, - // ), - // ], - // ), - // ), - // Expanded( - // child: SingleChildScrollView( - // padding: const EdgeInsets.all(24), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.stretch, - // children: [ - // Text( - // currentExercise.exerciseName, - // style: Theme.of(context).textTheme.displayMedium, - // textAlign: TextAlign.center, - // ), - // const SizedBox(height: 8), - // Text( - // 'Set ${_currentSetIndex + 1}/${currentExercise.sets.length}', - // style: Theme.of(context).textTheme.titleLarge, - // textAlign: TextAlign.center, - // ), - // const SizedBox(height: 24), - // Card( - // child: Padding( - // padding: const EdgeInsets.all(16), - // child: Column( - // children: [ - // Text('Target', - // style: Theme.of(context).textTheme.bodyMedium), - // const SizedBox(height: 8), - // Text( - // '${currentSet.targetWeightTotal.toStringAsFixed(1)} kg', - // style: Theme.of(context) - // .textTheme - // .displayMedium - // ?.copyWith(color: AppTheme.primaryColor), - // ), - // Text( - // '${currentSet.targetPercentage}% TM × ${currentSet.repsTarget} reps${currentSet.isAmrap ? '+' : ''}', - // style: Theme.of(context).textTheme.bodyMedium, - // ), - // ], - // ), - // ), - // ), - // const SizedBox(height: 24), - // if (plateResult.bandAssistance != null) - // Container( - // padding: const EdgeInsets.all(16), - // decoration: BoxDecoration( - // color: AppTheme.primaryColor.withOpacity(0.1), - // borderRadius: BorderRadius.circular(16), - // border: Border.all(color: AppTheme.primaryColor), - // ), - // child: Column( - // children: [ - // const Icon(Icons.help_outline, - // size: 48, color: AppTheme.primaryColor), - // const SizedBox(height: 8), - // Text( - // 'ASSISTANCE NEEDED', - // style: - // Theme.of(context).textTheme.titleMedium?.copyWith( - // color: AppTheme.primaryColor, - // fontWeight: FontWeight.bold, - // ), - // ), - // const SizedBox(height: 8), - // Text( - // 'Use ${plateResult.bandAssistance} Band', - // style: Theme.of(context).textTheme.headlineSmall, - // ), - // Text( - // '(approx. -${(plateResult.totalAchieved - currentSet.targetWeightTotal).abs().toStringAsFixed(1)} kg)', - // style: Theme.of(context).textTheme.bodySmall, - // ), - // ], - // ), - // ) - // else - // PlateVisualizer( - // plateConfiguration: plateResult.plateConfiguration, - // isTwoSided: currentExercise.exerciseId == 'squat', - // exerciseName: currentExercise.exerciseName, - // ), - // const SizedBox(height: 32), - // Text( - // 'Reps Completed', - // style: Theme.of(context).textTheme.titleLarge, - // textAlign: TextAlign.center, - // ), - // const SizedBox(height: 16), - // Row( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // IconButton( - // icon: const Icon(Icons.remove_circle), - // iconSize: 48, - // color: AppTheme.primaryColor, - // onPressed: _repsCompleted > 0 - // ? () => setState(() => _repsCompleted--) - // : null, - // ), - // const SizedBox(width: 24), - // SizedBox( - // width: 100, - // child: Text( - // _repsCompleted.toString(), - // style: Theme.of(context) - // .textTheme - // .displayLarge - // ?.copyWith( - // fontSize: 64, color: AppTheme.primaryColor), - // textAlign: TextAlign.center, - // ), - // ), - // const SizedBox(width: 24), - // IconButton( - // icon: const Icon(Icons.add_circle), - // iconSize: 48, - // color: AppTheme.primaryColor, - // onPressed: () => setState(() => _repsCompleted++), - // ), - // ], - // ), - // if (currentSet.isAmrap) - // Padding( - // padding: const EdgeInsets.only(top: 8), - // child: Text( - // 'AMRAP - Go for max reps!', - // style: Theme.of(context).textTheme.bodyMedium?.copyWith( - // color: AppTheme.secondaryColor, - // fontWeight: FontWeight.bold), - // textAlign: TextAlign.center, - // ), - // ), - // const SizedBox(height: 32), - // ElevatedButton( - // onPressed: _repsCompleted >= currentSet.repsTarget - // ? _completeSet - // : null, - // style: ElevatedButton.styleFrom( - // padding: const EdgeInsets.symmetric(vertical: 20)), - // child: const Text('COMPLETE SET'), - // ), - // ], - // ), - // ), - // ), - // ], - // ); - // } - - String _formatTime(int seconds) { - final minutes = seconds ~/ 60; - final secs = seconds % 60; - return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; - } -} 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 new file mode 100644 index 0000000..68c61e4 --- /dev/null +++ b/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart @@ -0,0 +1,232 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import '../../../../core/theme/app_theme.dart'; +import 'package:audioplayers/audioplayers.dart'; +import '../../../../core/constants/asset_paths.dart'; + +class EmomTimerWidget extends StatefulWidget { + final int intervalSeconds; + final int totalSets; + final int currentSet; + final VoidCallback onSetComplete; + final VoidCallback onWorkoutComplete; + + const EmomTimerWidget({ + super.key, + required this.intervalSeconds, + required this.totalSets, + required this.currentSet, + required this.onSetComplete, + required this.onWorkoutComplete, + }); + + @override + State createState() => _EmomTimerWidgetState(); +} + +class _EmomTimerWidgetState extends State + with TickerProviderStateMixin { + Timer? _timer; + late int _secondsRemaining; + bool _isRunning = false; + late AnimationController _pulseController; + late AudioPlayer _audioPlayer; + + @override + void initState() { + super.initState(); + _secondsRemaining = widget.intervalSeconds; + _audioPlayer = AudioPlayer(); + + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 500), + lowerBound: 1.0, + upperBound: 1.1, + ); + } + + @override + void dispose() { + _timer?.cancel(); + _pulseController.dispose(); + _audioPlayer.dispose(); + super.dispose(); + } + + Future _playSound(bool isLong) async { + try { + final path = isLong ? 'audio/beep_long.ogg' : 'audio/beep_short.ogg'; + + if (_audioPlayer.state == PlayerState.playing) { + await _audioPlayer.stop(); + } + await _audioPlayer.play(AssetSource(path)); + } catch (e) { + debugPrint('Audio error: $e'); + } + } + + void _startTimer() { + setState(() => _isRunning = true); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_secondsRemaining > 0) { + setState(() => _secondsRemaining--); + if (_secondsRemaining <= 3) { + _pulseController.forward().then((_) => _pulseController.reverse()); + _playSound(false); + } + } else { + _playSound(true); + _handleRoundComplete(); + } + }); + } + + void _handleRoundComplete() { + if (widget.currentSet < widget.totalSets) { + widget.onSetComplete(); + setState(() { + _secondsRemaining = widget.intervalSeconds; + }); + } else { + _timer?.cancel(); + setState(() => _isRunning = false); + widget.onWorkoutComplete(); + } + } + + void _pauseTimer() { + _timer?.cancel(); + setState(() => _isRunning = false); + } + + String _formatTime(int seconds) { + final m = seconds ~/ 60; + final s = seconds % 60; + return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; + } + + @override + Widget build(BuildContext context) { + final progress = 1.0 - (_secondsRemaining / widget.intervalSeconds); + + return Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppTheme.primaryColor), + ), + child: Text( + 'ROUND ${widget.currentSet} / ${widget.totalSets}', + style: const TextStyle( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), + ), + const SizedBox(height: 32), + ScaleTransition( + scale: _pulseController, + child: SizedBox( + width: 240, + height: 240, + child: Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 240, + height: 240, + child: CircularProgressIndicator( + value: 1.0, + strokeWidth: 12, + color: Colors.white10, + ), + ), + SizedBox( + width: 240, + height: 240, + child: CircularProgressIndicator( + value: progress, + strokeWidth: 12, + color: _secondsRemaining <= 3 + ? AppTheme.errorColor + : AppTheme.primaryColor, + strokeCap: StrokeCap.round, + ), + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _formatTime(_secondsRemaining), + style: Theme.of(context).textTheme.displayLarge?.copyWith( + fontSize: 64, + color: Colors.white, + fontFamily: 'monospace', + ), + ), + if (!_isRunning && + widget.currentSet == 1 && + _secondsRemaining == widget.intervalSeconds) + Text( + 'READY?', + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(color: Colors.grey), + ) + else if (!_isRunning) + Text( + 'PAUSED', + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(color: AppTheme.errorColor), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 48), + if (!_isRunning) + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: _startTimer, + icon: const Icon(Icons.play_arrow), + label: Text(widget.currentSet == 1 && + _secondsRemaining == widget.intervalSeconds + ? 'IGNITE ENGINE' + : 'RESUME'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.successColor, + foregroundColor: Colors.white, + ), + ), + ) + else + SizedBox( + width: double.infinity, + height: 56, + child: OutlinedButton.icon( + onPressed: _pauseTimer, + icon: const Icon(Icons.pause), + label: const Text('PAUSE'), + style: OutlinedButton.styleFrom( + foregroundColor: AppTheme.errorColor, + side: const BorderSide(color: AppTheme.errorColor), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/features/workout_runner/presentation/widgets/enemy_hp_bar.dart b/lib/src/features/workout_runner/presentation/widgets/enemy_hp_bar.dart index 08f213f..997eadb 100644 --- a/lib/src/features/workout_runner/presentation/widgets/enemy_hp_bar.dart +++ b/lib/src/features/workout_runner/presentation/widgets/enemy_hp_bar.dart @@ -53,7 +53,7 @@ class EnemyHPBar extends StatelessWidget { color: Colors.red[900], borderRadius: BorderRadius.circular(12), border: Border.all( - color: AppTheme.errorColor.withOpacity(0.5), + color: AppTheme.errorColor.withValues(alpha: 0.5), width: 2, ), ), @@ -72,7 +72,7 @@ class EnemyHPBar extends StatelessWidget { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: AppTheme.errorColor.withOpacity(0.5), + color: AppTheme.errorColor.withValues(alpha: 0.5), blurRadius: 8, ), ], 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 28941c0..f07446c 100644 --- a/lib/src/features/workout_runner/presentation/widgets/plate_visualizer.dart +++ b/lib/src/features/workout_runner/presentation/widgets/plate_visualizer.dart @@ -30,7 +30,7 @@ class PlateVisualizer extends StatelessWidget { Icon( isTwoSided ? Icons.fitness_center : Icons.accessibility, size: 64, - color: AppTheme.primaryColor.withOpacity(0.5), + color: AppTheme.primaryColor.withValues(alpha: 0.5), ), const SizedBox(height: 16), Text( diff --git a/lib/src/shared/data/repositories/workout_repository.dart b/lib/src/shared/data/repositories/workout_repository.dart index b1cddc3..a16904c 100644 --- a/lib/src/shared/data/repositories/workout_repository.dart +++ b/lib/src/shared/data/repositories/workout_repository.dart @@ -1,109 +1,3 @@ -// import 'package:flutter_riverpod/flutter_riverpod.dart'; -// import 'package:drift/drift.dart'; -// import '../local/app_database.dart'; -// import '../remote/api_client.dart'; -// import '../../../../main.dart'; -// import 'user_repository.dart'; - -// final workoutRepositoryProvider = Provider((ref) { -// final db = ref.watch(appDatabaseProvider); -// final apiClient = ref.watch(apiClientProvider); -// return WorkoutRepository(db: db, apiClient: apiClient); -// }); - -// class WorkoutRepository { -// final AppDatabase db; -// final ApiClient apiClient; - -// WorkoutRepository({required this.db, required this.apiClient}); - -// Future> getAllWorkouts() async { -// return await db.select(db.workouts).get(); -// } - -// Future> getWorkoutsForCycle(String cycleId) async { -// return await (db.select(db.workouts) -// ..where((w) => w.cycleId.equals(cycleId))) -// .get(); -// } - -// Future> getCompletedWorkouts(String userId) async { -// return await (db.select(db.workouts) -// ..where((w) => w.userId.equals(userId) & w.completedAt.isNotNull())) -// .get(); -// } - -// Future saveWorkout(WorkoutCollection workout) async { -// final companion = workout.toCompanion(true).copyWith( -// updatedAt: Value(DateTime.now()), -// isDirty: const Value(true), -// ); -// await db.into(db.workouts).insertOnConflictUpdate(companion); -// } - -// Future createWorkout({ -// required String userId, -// required String cycleId, -// required int week, -// required int day, -// required List exercises, -// }) async { -// final companion = WorkoutsCompanion( -// userId: Value(userId), -// cycleId: Value(cycleId), -// week: Value(week), -// day: Value(day), -// exercises: Value(exercises), -// scheduledDate: Value(DateTime.now()), -// xpEarned: const Value(0), -// notes: const Value(''), -// isDirty: const Value(true), -// createdAt: Value(DateTime.now()), -// updatedAt: Value(DateTime.now()), -// ); - -// final id = await db.into(db.workouts).insert(companion); -// return await (db.select(db.workouts)..where((w) => w.id.equals(id))) -// .getSingle(); -// } - -// Future completeWorkout( -// WorkoutCollection workout, { -// required int xpEarned, -// }) async { -// final companion = WorkoutsCompanion( -// id: Value(workout.id), -// completedAt: Value(DateTime.now()), -// xpEarned: Value(xpEarned), -// exercises: Value(workout.exercises), -// isDirty: const Value(true), -// updatedAt: Value(DateTime.now()), -// ); - -// await (db.update(db.workouts)..where((w) => w.id.equals(workout.id))) -// .write(companion); -// } - -// Future getWorkoutByWeekDay({ -// required String cycleId, -// String? localCycleId, -// required int week, -// required int day, -// }) async { -// return await (db.select(db.workouts) -// ..where((w) { -// final weekDayCheck = w.week.equals(week) & w.day.equals(day); - -// Expression cycleCheck = w.cycleId.equals(cycleId); -// if (localCycleId != null) { -// cycleCheck = cycleCheck | w.cycleId.equals(localCycleId); -// } - -// return weekDayCheck & cycleCheck; -// })) -// .getSingleOrNull(); -// } -// } import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:drift/drift.dart'; import '../local/app_database.dart'; @@ -210,4 +104,9 @@ class WorkoutRepository { ..limit(1)) .getSingleOrNull(); } + + Future getWorkoutById(int id) async { + return await (db.select(db.workouts)..where((w) => w.id.equals(id))) + .getSingleOrNull(); + } } diff --git a/lib/src/shared/domain/entities/exercise.dart b/lib/src/shared/domain/entities/exercise.dart index f58d3b7..6dd56b0 100644 --- a/lib/src/shared/domain/entities/exercise.dart +++ b/lib/src/shared/domain/entities/exercise.dart @@ -11,9 +11,9 @@ class Exercise with _$Exercise { required String exerciseName, @Default(0.0) double bodyweightAtSession, @Default([]) List sets, + int? intervalSeconds, }) = _Exercise; factory Exercise.fromJson(Map json) => _$ExerciseFromJson(json); } - diff --git a/lib/src/shared/domain/logic/wendler_calculator.dart b/lib/src/shared/domain/logic/wendler_calculator.dart index 8e55517..9a1eb59 100644 --- a/lib/src/shared/domain/logic/wendler_calculator.dart +++ b/lib/src/shared/domain/logic/wendler_calculator.dart @@ -2,7 +2,30 @@ import 'dart:math'; import '../entities/workout_set.dart'; import '../../../core/constants/app_constants.dart'; -enum ExerciseType { squat, pullup, dip, row, bench } +enum ExerciseType { + // Main Lifts + squat, + pullup, + dip, + row, + bench, + + // Hypertrophy Accessories + deadlift_romanian, + curl_barbell, + press_overhead, + face_pull, + ab_wheel, + plank, + + // Conditioning (Kettlebell) + kb_swing, + kb_snatch, + kb_thruster, + kb_clean_press +} + +enum AccessoryTemplate { none, hypertrophy, conditioning } class WendlerCalculator { static const Map> weekPercentages = { diff --git a/pubspec.lock b/pubspec.lock index a916baf..9d80ba4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: "5441fa0ceb8807a5ad701199806510e56afde2b4913d9d17c2f19f2902cf0ae4" + url: "https://pub.dev" + source: hosted + version: "6.5.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" + url: "https://pub.dev" + source: hosted + version: "7.1.1" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7" + url: "https://pub.dev" + source: hosted + version: "4.2.1" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 723265d..d5b522f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: flutter: sdk: flutter + audioplayers: ^6.0.0 # State Management flutter_riverpod: ^2.5.1 @@ -67,6 +68,7 @@ flutter: - assets/images/plates/ - assets/images/enemies/ - assets/images/backgrounds/ + - assets/audio/ # fonts: # - family: PixelFont