diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 158c829..610d9a0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,48 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - context.go('/login'), - child: const Text('Already a hero? Login here', + child: Text(l10n.loginPrompt, style: TextStyle(color: Colors.white54)), ), ], diff --git a/lib/src/features/wiki/presentation/widgets/exercise_guide_sheet.dart b/lib/src/features/wiki/presentation/widgets/exercise_guide_sheet.dart new file mode 100644 index 0000000..bd74bb6 --- /dev/null +++ b/lib/src/features/wiki/presentation/widgets/exercise_guide_sheet.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../../../shared/domain/models/exercise_guide.dart'; + +class ExerciseGuideSheet extends StatelessWidget { + final String exerciseId; + + const ExerciseGuideSheet({super.key, required this.exerciseId}); + + @override + Widget build(BuildContext context) { + String lookupId = exerciseId; + if (exerciseId.contains('kb_snatch')) lookupId = 'kb_snatch'; + // Weitere Mappings hier falls nötig... + + final guide = exerciseLibrary[lookupId]; + + if (guide == null) { + return Container( + padding: const EdgeInsets.all(32), + child: const Text('No ancient scroll found for this technique.', + textAlign: TextAlign.center, style: TextStyle(color: Colors.grey)), + ); + } + + return DraggableScrollableSheet( + initialChildSize: 0.85, + minChildSize: 0.5, + maxChildSize: 0.95, + builder: (context, scrollController) { + return Container( + decoration: const BoxDecoration( + color: AppTheme.surfaceColor, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + children: [ + const SizedBox(height: 12), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[700], + borderRadius: BorderRadius.circular(2), + ), + ), + Expanded( + child: ListView( + controller: scrollController, + padding: const EdgeInsets.all(24), + children: [ + Text( + guide.title.toUpperCase(), + style: + Theme.of(context).textTheme.headlineMedium?.copyWith( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + _buildDifficultyBadge(guide.difficulty), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.backgroundColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white12), + ), + child: Text( + '"${guide.rpgLore}"', + style: const TextStyle( + fontStyle: FontStyle.italic, + color: AppTheme.textSecondary, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 32), + _buildSectionTitle(context, 'EXECUTION'), + const SizedBox(height: 16), + ...guide.steps + .asMap() + .entries + .map((entry) => _buildStep(entry.key + 1, entry.value)), + const SizedBox(height: 32), + _buildSectionTitle(context, 'COMMON MISTAKES'), + const SizedBox(height: 16), + ...guide.commonMistakes.map((m) => _buildMistake(m)), + const SizedBox(height: 32), + _buildSectionTitle(context, 'ATTRIBUTES AFFECTED'), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: guide.muscles + .map((m) => Chip( + label: Text(m), + backgroundColor: AppTheme.primaryColor + .withValues(alpha: 0.1), + labelStyle: const TextStyle( + color: AppTheme.primaryColor), + side: BorderSide.none, + )) + .toList(), + ), + const SizedBox(height: 40), + ], + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildDifficultyBadge(String diff) { + Color color; + switch (diff) { + case 'Novice': + color = Colors.green; + break; + case 'Adept': + color = Colors.orange; + break; + case 'Master': + color = AppTheme.errorColor; + break; + default: + color = Colors.grey; + } + + return Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withValues(alpha: 0.5)), + ), + child: Text( + diff.toUpperCase(), + style: TextStyle( + color: color, fontSize: 12, fontWeight: FontWeight.bold), + ), + ), + ); + } + + Widget _buildSectionTitle(BuildContext context, String title) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), + const Divider( + color: AppTheme.primaryColor, thickness: 2, endIndent: 250), + ], + ); + } + + Widget _buildStep(int index, String text) { + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 24, + height: 24, + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppTheme.primaryColor, + shape: BoxShape.circle, + ), + child: Text('$index', + style: const TextStyle( + color: Colors.black, fontWeight: FontWeight.bold)), + ), + const SizedBox(width: 16), + Expanded( + child: Text(text, + style: const TextStyle(color: Colors.white70, height: 1.4))), + ], + ), + ); + } + + Widget _buildMistake(String text) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + const Icon(Icons.close, color: AppTheme.errorColor, size: 20), + const SizedBox(width: 12), + Expanded( + child: Text(text, style: const TextStyle(color: Colors.white70))), + ], + ), + ); + } +} 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 df755d1..8cc0237 100644 --- a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart +++ b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart @@ -20,6 +20,7 @@ import '../widgets/enemy_hp_bar.dart'; import '../../../gamification/application/quest_service.dart'; import '../widgets/emom_timer_widget.dart'; import '../widgets/timer_widget.dart'; +import '../../../wiki/presentation/widgets/exercise_guide_sheet.dart'; class BattleScreen extends ConsumerStatefulWidget { final int week; @@ -105,6 +106,15 @@ class _BattleScreenState extends ConsumerState { } } + void _showExerciseGuide(String exerciseId) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => ExerciseGuideSheet(exerciseId: exerciseId), + ); + } + List> _getExerciseConfig(int day, UserCollection user) { final variants = user.exerciseVariants ?? {}; @@ -875,6 +885,13 @@ class _BattleScreenState extends ConsumerState { ), textAlign: TextAlign.center, ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.info_outline, + color: Colors.white54), + onPressed: () => + _showExerciseGuide(currentExercise.exerciseId), + ), Text( 'Set ${_currentSetIndex + 1} of ${currentExercise.sets.length}', style: Theme.of(context) @@ -1108,6 +1125,14 @@ class _BattleScreenState extends ConsumerState { fontSize: 16, color: Colors.white), ), + IconButton( + icon: const Icon(Icons.info_outline, + size: 20, color: AppTheme.primaryColor), + onPressed: () => + _showExerciseGuide(currentExercise.exerciseId), + padding: const EdgeInsets.all(4), + constraints: const BoxConstraints(), + ), Text( '${currentSet.repsTarget} Reps per Round', style: const TextStyle(color: Colors.grey), diff --git a/lib/src/shared/domain/models/exercise_guide.dart b/lib/src/shared/domain/models/exercise_guide.dart new file mode 100644 index 0000000..63fc741 --- /dev/null +++ b/lib/src/shared/domain/models/exercise_guide.dart @@ -0,0 +1,233 @@ +import '../logic/wendler_calculator.dart'; + +class ExerciseGuide { + final String title; + final String difficulty; // z.B. "Novice", "Adept", "Master" + final String rpgLore; // Ein kleiner Flavor-Text + final List steps; + final List muscles; + final List commonMistakes; + + const ExerciseGuide({ + required this.title, + required this.difficulty, + required this.rpgLore, + required this.steps, + required this.muscles, + required this.commonMistakes, + }); +} + +final Map exerciseLibrary = { + 'pullup': const ExerciseGuide( + title: 'Weighted Pull-Up', + difficulty: 'Adept', + rpgLore: + 'Den Körper gegen die Schwerkraft zu ziehen, ist der ultimative Beweis für Oberkörperkraft.', + steps: [ + 'Greife die Stange etwas weiter als schulterbreit (Obergriff).', + 'Aktiviere den Core und ziehe die Schulterblätter nach unten/hinten.', + 'Ziehe dich hoch, bis das Kinn über der Stange ist.', + 'Lasse dich kontrolliert wieder ab.', + ], + muscles: ['Latissimus', 'Bizeps', 'Unterarme'], + commonMistakes: [ + 'Schwingen (Kipping)', + 'Halbe Wiederholungen', + 'Schultern hochgezogen lassen' + ], + ), + 'dip': const ExerciseGuide( + title: 'Weighted Dip', + difficulty: 'Adept', + rpgLore: 'Ein fundamentaler Druck-Move, um Mauern zu überwinden.', + steps: [ + 'Stütze dich auf die Barren, Arme gestreckt.', + 'Lehne dich leicht nach vorne für mehr Brust-Fokus.', + 'Senke den Körper ab, bis die Schultern unter den Ellbogen sind.', + 'Drücke dich explosiv zurück in die Ausgangsposition.', + ], + muscles: ['Brust', 'Trizeps', 'Vordere Schulter'], + commonMistakes: ['Zu wenig Tiefe', 'Ellbogen wandern zu weit nach außen'], + ), + 'squat': const ExerciseGuide( + title: 'Low Bar Back Squat', + difficulty: 'Master', + rpgLore: + 'Die Mutter aller Schlachten. Trainiert den gesamten Körperpanzer.', + steps: [ + 'Lege die Hantel auf dem hinteren Deltamuskel ab (nicht im Nacken).', + 'Füße schulterbreit, Zehen leicht nach außen.', + 'Atme tief ein (Bracing) und schiebe die Hüfte nach hinten.', + 'Gehe in die Hocke (Hüfte unter Kniehöhe).', + 'Drücke dich aus der Ferse/Mittelfuß wieder hoch.', + ], + muscles: ['Quadrizeps', 'Gesäß', 'Core', 'Rückenstrecker'], + commonMistakes: [ + 'Knie fallen nach innen', + 'Rücken rundet ein', + 'Zu wenig Tiefe' + ], + ), + 'bench': const ExerciseGuide( + title: 'Bench Press', + difficulty: 'Novice', + rpgLore: 'Der Standard-Test für reine Druckkraft.', + steps: [ + 'Lege dich auf die Bank, Augen unter der Stange.', + 'Füße fest am Boden, leichter Bogen im Rücken (Brücke).', + 'Senke die Hantel kontrolliert zur unteren Brust.', + 'Drücke die Hantel explosiv nach oben.', + ], + muscles: ['Brust', 'Trizeps', 'Vordere Schulter'], + commonMistakes: [ + 'Ellbogen 90° abgespreizt (Verletzungsgefahr)', + 'Hintern hebt ab' + ], + ), + 'ohp': const ExerciseGuide( + title: 'Overhead Press', + difficulty: 'Adept', + rpgLore: 'Ein Objekt über den Kopf zu stemmen erfordert pure Stabilität.', + steps: [ + 'Stange liegt auf dem vorderen Schultermuskel.', + 'Fester Stand, Gesäß und Bauch maximal anspannen.', + 'Kopf leicht zurücknehmen, Stange vertikal nach oben drücken.', + 'Oben den Kopf "durch das Fenster" der Arme schieben.', + ], + muscles: ['Schultern', 'Trizeps', 'Core'], + commonMistakes: ['Hohlkreuz (Rücklage)', 'Beine helfen mit (Push Press)'], + ), + 'rdl': const ExerciseGuide( + title: 'Romanian Deadlift', + difficulty: 'Adept', + rpgLore: 'Baut die hintere Kette auf, essenziell für Stabilität.', + steps: [ + 'Startposition stehend mit Hantel.', + 'Schiebe die Hüfte weit nach hinten, Beine bleiben fast gestreckt.', + 'Senke die Hantel am Bein entlang bis knapp unter das Knie.', + 'Spüre den Zug im Beinbeuger und richte dich wieder auf.', + ], + muscles: ['Beinbeuger (Hamstrings)', 'Gesäß', 'Unterer Rücken'], + commonMistakes: ['Rücken rundet ein', 'Hantel zu weit vom Körper weg'], + ), + 'row': const ExerciseGuide( + title: 'Pendlay Row', + difficulty: 'Adept', + rpgLore: 'Explosive Zugkraft vom Boden. Für einen starken Rücken.', + steps: [ + 'Oberkörper parallel zum Boden, Rücken gerade.', + 'Hantel liegt bei jeder Wiederholung tot auf dem Boden.', + 'Ziehe die Hantel explosiv zum unteren Brustbein.', + 'Kontrolliert ablegen, Spannung kurz lösen, neu ansetzen.', + ], + muscles: ['Latissimus', 'Trapez', 'Hintere Schulter'], + commonMistakes: ['Oberkörper richtet sich auf', 'Reißen mit Schwung'], + ), + 'curl': const ExerciseGuide( + title: 'Barbell Curl', + difficulty: 'Novice', + rpgLore: 'Isolierte Kraft für den finalen Schlag.', + steps: [ + 'Stehender Stand, Hantel im Untergriff.', + 'Ellbogen bleiben fixiert am Körper.', + 'Hantel zur Brust curlen, oben kurz halten.', + 'Langsam ablassen.', + ], + muscles: ['Bizeps'], + commonMistakes: ['Schwingen aus der Hüfte', 'Ellbogen wandern nach vorne'], + ), + 'kb_swing': const ExerciseGuide( + title: 'Kettlebell Swing', + difficulty: 'Adept', + rpgLore: 'Ballistische Kraft und Ausdauer. Die Hüfte ist der Motor.', + steps: [ + 'Hüftbreiter Stand, KB vor dir am Boden.', + 'Hike-Pass: Ziehe die KB durch die Beine nach hinten.', + 'Hüfte explosiv strecken (Snap!), KB fliegt durch Hüftkraft auf Brusthöhe.', + 'KB kontrolliert zurückschwingen lassen.', + ], + muscles: ['Gesäß', 'Beinbeuger', 'Core', 'Ausdauer'], + commonMistakes: ['Kniebeuge statt Hüftbeuge', 'Arme heben das Gewicht'], + ), + 'kb_snatch': const ExerciseGuide( + title: 'Kettlebell Snatch', + difficulty: 'Master', + rpgLore: 'Der Zar der Kettlebell-Übungen. Totale Körperkontrolle.', + steps: [ + 'Starte wie beim Swing (Einarmig).', + 'Hüftkraft beschleunigt die Kugel nach oben.', + 'Bei Kopfhöhe: Durchstoßen der Hand ("Punch through").', + 'Sanftes Auffangen im Lockout über Kopf.', + ], + muscles: ['Gesamter Körper', 'Schultern', 'Griffkraft'], + commonMistakes: ['Kugel knallt auf den Unterarm', 'Zu wenig Hüftkraft'], + ), + 'kb_thruster': const ExerciseGuide( + title: 'Kettlebell Thruster', + difficulty: 'Master', + rpgLore: 'Eine brutale Kombination aus Squat und Press.', + steps: [ + 'KB in der Rack-Position (vor der Brust).', + 'Tiefe Kniebeuge.', + 'Beim Aufstehen den Schwung nutzen, um KB über Kopf zu drücken.', + 'Zurück in die Rack-Position beim Absenken in den nächsten Squat.', + ], + muscles: ['Beine', 'Schultern', 'Lunge (Cardio)'], + commonMistakes: [ + 'Pause zwischen Squat und Press', + 'Rücken rundet im Squat' + ], + ), + 'kb_clean_press': const ExerciseGuide( + title: 'KB Clean & Press', + difficulty: 'Adept', + rpgLore: 'Zwei Bewegungen in Harmonie.', + steps: [ + 'Clean: Ziehe die KB vom Boden in die Rack-Position vor der Brust.', + 'Press: Drücke sie strikt über den Kopf.', + 'Senke sie in Rack, dann zum Boden (oder Swing).', + ], + muscles: ['Schultern', 'Rücken', 'Beine'], + commonMistakes: ['Clean knallt auf Arm', 'Hohlkreuz beim Press'], + ), + 'face_pull': const ExerciseGuide( + title: 'Band Face Pull', + difficulty: 'Novice', + rpgLore: 'Schützt die Schultern vor dem Verschleiß des Kampfes.', + steps: [ + 'Band auf Kopfhöhe befestigen.', + 'Ziehe das Band zum Gesicht (Richtung Stirn/Augen).', + 'Ellbogen hoch und weit nach außen ziehen.', + 'Schulterblätter hinten zusammenkneifen.', + ], + muscles: ['Hintere Schulter', 'Rotatorenmanschette'], + commonMistakes: ['Ellbogen zu tief', 'Kopf nach vorne schieben'], + ), + 'ab_wheel': const ExerciseGuide( + title: 'Ab Wheel Rollout', + difficulty: 'Adept', + rpgLore: 'Ein Stahlkern, der jeden Treffer absorbiert.', + steps: [ + 'Knie am Boden, Rad vor den Knien.', + 'Rolle nach vorne, halte den Rücken rund/stabil (Hollow Body).', + 'Gehe nur so weit, wie du den Rücken stabil halten kannst.', + 'Ziehe dich aus dem Bauchmuskel zurück.', + ], + muscles: ['Core (Anti-Extension)'], + commonMistakes: ['Hohlkreuz (Gefährlich!)', 'Ziehen aus den Armen'], + ), + 'plank': const ExerciseGuide( + title: 'Plank', + difficulty: 'Novice', + rpgLore: 'Unbeweglich wie ein Fels in der Brandung.', + steps: [ + 'Unterarmstütz, Körper bildet eine Linie.', + 'Gesäß und Bauch fest anspannen.', + 'Schulterblätter auseinanderdrücken.', + 'Atmen nicht vergessen!', + ], + muscles: ['Core'], + commonMistakes: ['Hüfte hängt durch', 'Gesäß zu hoch'], + ), +};