diff --git a/assets/audio/beep_long.ogg b/assets/audio/beep_long.ogg new file mode 100644 index 0000000..1fe6edf Binary files /dev/null and b/assets/audio/beep_long.ogg differ diff --git a/assets/audio/beep_short.ogg b/assets/audio/beep_short.ogg new file mode 100644 index 0000000..73ef94a Binary files /dev/null and b/assets/audio/beep_short.ogg differ 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