diff --git a/lib/src/core/constants/asset_paths.dart b/lib/src/core/constants/asset_paths.dart index 7ad0fa4..a86cabb 100644 --- a/lib/src/core/constants/asset_paths.dart +++ b/lib/src/core/constants/asset_paths.dart @@ -1,55 +1,6 @@ -// class AssetPaths { -// // Backgrounds -// static const String bgStreetParkDay = -// 'assets/images/backgrounds/street_park_day.png'; -// static const String bgStreetParkNight = -// 'assets/images/backgrounds/street_park_night.png'; -// static const String bgUndergroundGym = -// 'assets/images/backgrounds/underground_gym.png'; -// static const String bgCommercialGym = -// 'assets/images/backgrounds/commercial_gym.png'; - -// // Avatars -// static const String avatarMaleBase = 'assets/images/avatars/male_base.png'; -// static const String avatarFemaleBase = -// 'assets/images/avatars/female_base.png'; - -// // Plates -// static const String plate25kg = 'assets/images/plates/plate_25kg.png'; -// static const String plate20kg = 'assets/images/plates/plate_20kg.png'; -// static const String plate15kg = 'assets/images/plates/plate_15kg.png'; -// static const String plate10kg = 'assets/images/plates/plate_10kg.png'; -// static const String plate5kg = 'assets/images/plates/plate_5kg.png'; -// static const String plate2_5kg = 'assets/images/plates/plate_2_5kg.png'; -// static const String plate1_25kg = 'assets/images/plates/plate_1_25kg.png'; - -// // Enemies -// static const String enemyIronGolem = 'assets/images/enemies/iron_golem.png'; -// static const String enemyGravityDemon = -// 'assets/images/enemies/gravity_demon.png'; -// static const String enemyPressurePhantom = -// 'assets/images/enemies/pressure_phantom.png'; - -// // Icons -// static const String iconXP = 'assets/images/icons/xp.png'; -// static const String iconLevel = 'assets/images/icons/level.png'; -// } - -// class PlateColors { -// static final Map colors = { -// 25.0: 0xFFD32F2F, // Red -// 20.0: 0xFF1976D2, // Blue -// 15.0: 0xFFFBC02D, // Yellow -// 10.0: 0xFF388E3C, // Green -// 5.0: 0xFFFAFAFA, // White -// 2.5: 0xFF212121, // Black -// 1.25: 0xFF9E9E9E, // Silver -// }; -// } import 'dart:ui'; class AssetPaths { - // Backgrounds static const String bgSplash = 'assets/images/backgrounds/splash.png'; static const String bgStreetParkDay = 'assets/images/backgrounds/street_park_day.png'; @@ -60,25 +11,20 @@ class AssetPaths { static const String bgCommercialGym = 'assets/images/backgrounds/commercial_gym.png'; - // Avatars - Bases static const String avatarMaleBase = 'assets/images/avatars/base/male.png'; static const String avatarFemaleBase = 'assets/images/avatars/base/female.png'; - // Avatars - Hair (Beispiele) static const String hairShort = 'assets/images/avatars/hair/short.png'; static const String hairLong = 'assets/images/avatars/hair/long.png'; - static const String hairBald = - 'assets/images/avatars/hair/bald.png'; // Transparent/Empty + static const String hairBald = 'assets/images/avatars/hair/bald.png'; - // Avatars - Clothing (Beispiele) static const String outfitBasicTee = 'assets/images/avatars/clothing/basic_tee.png'; static const String outfitHoodie = 'assets/images/avatars/clothing/hoodie.png'; static const String outfitTank = 'assets/images/avatars/clothing/tank.png'; - // Plates static const String plate25kg = 'assets/images/plates/plate_25kg.png'; static const String plate20kg = 'assets/images/plates/plate_20kg.png'; static const String plate15kg = 'assets/images/plates/plate_15kg.png'; @@ -87,7 +33,6 @@ class AssetPaths { static const String plate2_5kg = 'assets/images/plates/plate_2_5kg.png'; static const String plate1_25kg = 'assets/images/plates/plate_1_25kg.png'; - // Enemies & Icons (wie vorher...) static const String enemyIronGolem = 'assets/images/enemies/iron_golem.png'; static const String enemyGravityDemon = 'assets/images/enemies/gravity_demon.png'; diff --git a/lib/src/core/routing/app_router.dart b/lib/src/core/routing/app_router.dart index fb30b07..7a12beb 100644 --- a/lib/src/core/routing/app_router.dart +++ b/lib/src/core/routing/app_router.dart @@ -117,7 +117,6 @@ final routerProvider = Provider((ref) { ); }); -// Splash Screen to determine initial route class SplashScreen extends ConsumerStatefulWidget { const SplashScreen({super.key}); @@ -141,10 +140,8 @@ class _SplashScreenState extends ConsumerState { final user = await userRepo.getLocalUser(); if (user == null) { - // No user, go to login context.go('/login'); } else { - // User exists, go to hub context.go('/hub'); } } @@ -154,27 +151,21 @@ class _SplashScreenState extends ConsumerState { return Scaffold( body: Stack( children: [ - // 1. Hintergrundbild Positioned.fill( child: Image.asset( - AssetPaths.bgSplash, // Nutzt den Splash + AssetPaths.bgSplash, fit: BoxFit.cover, ), ), - - // 2. Overlay (Dunkel), damit Text lesbar ist Positioned.fill( child: Container( color: Colors.black.withOpacity(0.5), ), ), - - // 3. Inhalt Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Logo Container (mit leichtem Glow) Container( width: 120, height: 120, @@ -228,45 +219,4 @@ class _SplashScreenState extends ConsumerState { ), ); } - // @override - // Widget build(BuildContext context) { - // return Scaffold( - // backgroundColor: const Color(0xFF121212), - // body: Center( - // child: Column( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // // Logo placeholder - // Container( - // width: 120, - // height: 120, - // decoration: BoxDecoration( - // color: const Color(0xFF00E5FF), - // borderRadius: BorderRadius.circular(24), - // ), - // child: const Icon( - // Icons.fitness_center, - // size: 64, - // color: Colors.black, - // ), - // ), - // const SizedBox(height: 24), - // Text( - // 'S.L.R.P.G.', - // style: Theme.of(context).textTheme.displayLarge, - // ), - // const SizedBox(height: 8), - // Text( - // 'Streetlifting RPG', - // style: Theme.of(context).textTheme.bodyMedium, - // ), - // const SizedBox(height: 48), - // const CircularProgressIndicator( - // color: Color(0xFF00E5FF), - // ), - // ], - // ), - // ), - // ); - // } } diff --git a/lib/src/core/theme/app_theme.dart b/lib/src/core/theme/app_theme.dart index 6895a39..104f8e0 100644 --- a/lib/src/core/theme/app_theme.dart +++ b/lib/src/core/theme/app_theme.dart @@ -82,7 +82,6 @@ class AppTheme { ), ), cardTheme: CardThemeData( - // CardTheme -> CardThemeData color: surfaceColor, elevation: 4, shape: RoundedRectangleBorder( diff --git a/lib/src/features/authentication/presentation/screens/login_screen.dart b/lib/src/features/authentication/presentation/screens/login_screen.dart index e8a4005..cc4d1a2 100644 --- a/lib/src/features/authentication/presentation/screens/login_screen.dart +++ b/lib/src/features/authentication/presentation/screens/login_screen.dart @@ -86,7 +86,6 @@ class _LoginScreenState extends ConsumerState { ), const SizedBox(height: 24), - // Title Text( 'WELCOME BACK', style: Theme.of(context).textTheme.displayMedium, @@ -100,7 +99,6 @@ class _LoginScreenState extends ConsumerState { ), const SizedBox(height: 48), - // Email Field TextFormField( controller: _emailController, keyboardType: TextInputType.emailAddress, @@ -120,7 +118,6 @@ class _LoginScreenState extends ConsumerState { ), const SizedBox(height: 16), - // Password Field TextFormField( controller: _passwordController, obscureText: _obscurePassword, @@ -150,7 +147,6 @@ class _LoginScreenState extends ConsumerState { ), const SizedBox(height: 32), - // Login Button ElevatedButton( onPressed: _isLoading ? null : _handleLogin, child: _isLoading @@ -166,7 +162,6 @@ class _LoginScreenState extends ConsumerState { ), const SizedBox(height: 16), - // Register Link Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -189,4 +184,3 @@ class _LoginScreenState extends ConsumerState { ); } } - diff --git a/lib/src/features/authentication/presentation/screens/profile_screen.dart b/lib/src/features/authentication/presentation/screens/profile_screen.dart index e1a963c..daf80ea 100644 --- a/lib/src/features/authentication/presentation/screens/profile_screen.dart +++ b/lib/src/features/authentication/presentation/screens/profile_screen.dart @@ -8,7 +8,7 @@ import '../../../../shared/data/local/collections/user_collection.dart'; import '../../../gamification/domain/entities/avatar_config.dart'; import '../../../gamification/presentation/widgets/avatar_editor.dart'; import '../../../gamification/presentation/widgets/avatar_renderer.dart'; -import 'dart:convert'; // Für jsonDecode +import 'dart:convert'; class ProfileScreen extends ConsumerStatefulWidget { const ProfileScreen({super.key}); @@ -39,14 +39,6 @@ class _ProfileScreenState extends ConsumerState { }); } } - // Future _loadUser() async { - // final user = await ref.read(userRepositoryProvider).getLocalUser(); - // if (user != null && mounted) { - // setState(() { - // _currentBodyweight = user.currentBodyweight; - // }); - // } - // } Future _saveBodyweight() async { setState(() => _isLoading = true); @@ -177,7 +169,7 @@ class _ProfileScreenState extends ConsumerState { showModalBottomSheet( context: context, - isScrollControlled: true, // Wichtig für Fullscreen-Feeling + isScrollControlled: true, useSafeArea: true, backgroundColor: AppTheme.backgroundColor, builder: (context) => Scaffold( @@ -190,10 +182,6 @@ class _ProfileScreenState extends ConsumerState { actions: [ TextButton( onPressed: () { - // Speichern wird hier ausgelöst durch den Save-Callback im Editor Wrapper - // Aber da der Editor im BottomSheet state hat, müssen wir die Config rausbekommen. - // Einfacher: Wir wrappen den Editor in ein Stateful Widget im Dialog oder übergeben einen Callback. - // Da wir hier im ProfileScreen sind, können wir eine temporäre Variable nutzen und beim Schließen speichern. Navigator.pop(context, _tempAvatarConfig); }, child: const Text('SAVE', @@ -205,14 +193,12 @@ class _ProfileScreenState extends ConsumerState { ), body: AvatarEditor( initialConfig: currentConfig, - onChanged: (conf) => _tempAvatarConfig = - conf, // _tempAvatarConfig muss in State definiert werden + onChanged: (conf) => _tempAvatarConfig = conf, ), ), ).then((result) async { if (result is AvatarConfig) { setState(() => _isLoading = true); - // Speichern _user!.avatarConfigJson = jsonEncode(result.toJson()); _user!.isDirty = true; await ref.read(userRepositoryProvider).saveLocalUser(_user!); @@ -267,13 +253,6 @@ class _ProfileScreenState extends ConsumerState { child: IconButton( icon: const Icon(Icons.edit, size: 16), onPressed: _showAvatarEditor, - // onPressed: () { - // ScaffoldMessenger.of(context).showSnackBar( - // const SnackBar( - // content: Text( - // 'Avatar customization coming soon!')), - // ); - // }, ), ), ), @@ -281,8 +260,6 @@ class _ProfileScreenState extends ConsumerState { ), ), const SizedBox(height: 32), - - // Bodyweight Section Text('Physical Stats', style: Theme.of(context) .textTheme @@ -330,8 +307,6 @@ class _ProfileScreenState extends ConsumerState { ), ), const SizedBox(height: 32), - - // Account Actions Text('Account Security', style: Theme.of(context) .textTheme @@ -345,8 +320,6 @@ class _ProfileScreenState extends ConsumerState { onTap: _showChangePasswordDialog, ), const Divider(), - - // Danger Zone const SizedBox(height: 24), Text('Danger Zone', style: Theme.of(context) @@ -414,7 +387,6 @@ class _ProfileScreenState extends ConsumerState { ), ), const SizedBox(height: 32), - OutlinedButton.icon( onPressed: () async { await userRepo.logout(); diff --git a/lib/src/features/dashboard/presentation/screens/hub_screen.dart b/lib/src/features/dashboard/presentation/screens/hub_screen.dart index 59bd10a..8fc4725 100644 --- a/lib/src/features/dashboard/presentation/screens/hub_screen.dart +++ b/lib/src/features/dashboard/presentation/screens/hub_screen.dart @@ -170,7 +170,7 @@ class _HubScreenState extends ConsumerState { context.go('/battle', extra: { 'week': targetWeek, 'day': targetDay, - 'workoutId': workout!.id, + 'workoutId': workout.id, }); } } catch (e) { @@ -223,11 +223,10 @@ class _HubScreenState extends ConsumerState { children: [ Positioned.fill( child: Image.asset( - AssetPaths.bgStreetParkDay, // Das düstere Gym + AssetPaths.bgStreetParkDay, fit: BoxFit.cover, ), ), - // Dunkler Overlay, damit die UI-Elemente gut lesbar sind Positioned.fill( child: Container( decoration: BoxDecoration( @@ -235,26 +234,13 @@ class _HubScreenState extends ConsumerState { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Colors.black.withOpacity(0.6), // Oben etwas heller - Colors.black.withOpacity( - 0.85), // Unten fast schwarz für Buttons + Colors.black.withOpacity(0.6), + Colors.black.withOpacity(0.85), ], ), ), ), ), - // Container( - // decoration: BoxDecoration( - // gradient: LinearGradient( - // begin: Alignment.topCenter, - // end: Alignment.bottomCenter, - // colors: [ - // const Color(0xFF1A237E), - // AppTheme.backgroundColor, - // ], - // ), - // ), - // ), Column( children: [ Padding( @@ -289,23 +275,8 @@ class _HubScreenState extends ConsumerState { const Spacer(flex: 1), AvatarRenderer( config: avatarConfig, - size: 160, // Etwas größer für den Hub + size: 160, ), - // Container( - // width: 120, - // height: 120, - // decoration: BoxDecoration( - // color: AppTheme.primaryColor.withOpacity(0.2), - // shape: BoxShape.circle, - // border: - // Border.all(color: AppTheme.primaryColor, width: 3), - // ), - // child: const Icon( - // Icons.fitness_center, - // size: 64, - // color: AppTheme.primaryColor, - // ), - // ), const SizedBox(height: 24), LevelDisplay(level: user.level), const SizedBox(height: 16), @@ -395,7 +366,7 @@ class _HubScreenState extends ConsumerState { }, ), _NavButton( - icon: Icons.auto_stories, // Buch Icon + icon: Icons.auto_stories, label: 'Codex', onTap: () => context.go('/codex'), ), diff --git a/lib/src/features/gamification/domain/entities/avatar_config.dart b/lib/src/features/gamification/domain/entities/avatar_config.dart index e7ab4a0..f731cf2 100644 --- a/lib/src/features/gamification/domain/entities/avatar_config.dart +++ b/lib/src/features/gamification/domain/entities/avatar_config.dart @@ -1,48 +1,3 @@ -// import 'package:flutter/material.dart'; -// import '../../../../core/constants/asset_paths.dart'; - -// class AvatarConfig { -// final String gender; -// final String skinTone; -// final String hairStyle; -// final String clothing; - -// const AvatarConfig({ -// this.gender = 'male', -// this.skinTone = 'medium', -// this.hairStyle = 'short_01', -// this.clothing = 'basic_tee', -// }); - -// factory AvatarConfig.fromJson(Map json) { -// return AvatarConfig( -// gender: json['gender'] ?? 'male', -// skinTone: json['skin_tone'] ?? 'medium', -// hairStyle: json['hair_style'] ?? 'short_01', -// clothing: json['clothing'] ?? 'basic_tee', -// ); -// } - -// Map toJson() { -// return { -// 'gender': gender, -// 'skin_tone': skinTone, -// 'hair_style': hairStyle, -// 'clothing': clothing, -// }; -// } - -// // Helper getters -// String get baseAsset => gender == 'female' -// ? AssetPaths.avatarFemaleBase -// : AssetPaths.avatarMaleBase; - -// Color get skinColor => AvatarConstants.skinTones[skinTone] ?? const Color(0xFFD1A384); - -// String? get hairAsset => AvatarConstants.hairStyles[hairStyle]; - -// String? get clothingAsset => AvatarConstants.clothing[clothing]; -// } import '../../../../core/constants/asset_paths.dart'; class AvatarConfig { diff --git a/lib/src/features/gamification/presentation/screens/codex_screen.dart b/lib/src/features/gamification/presentation/screens/codex_screen.dart index a1a1944..5b7b478 100644 --- a/lib/src/features/gamification/presentation/screens/codex_screen.dart +++ b/lib/src/features/gamification/presentation/screens/codex_screen.dart @@ -22,10 +22,10 @@ class CodexScreen extends StatelessWidget { _LoreCard( name: 'Iron Golem', title: 'The Weight of the Earth', - description: - 'Forged from the tectonic plates of the Deep Earth, the Iron Golem exists only to crush the weak. ' - 'It embodies the unrelenting force of gravity acting on a heavy load.\n\n' - 'It respects only one thing: The raw power of the LEGS that can stand up against its crushing weight.', + description: + 'Forged from the tectonic plates of the Deep Earth, the Iron Golem exists only to crush the weak. ' + 'It embodies the unrelenting force of gravity acting on a heavy load.\n\n' + 'It respects only one thing: The raw power of the LEGS that can stand up against its crushing weight.', assetPath: AssetPaths.enemyIronGolem, exercise: 'Squat Nemesis', color: Colors.redAccent, @@ -34,10 +34,10 @@ class CodexScreen extends StatelessWidget { _LoreCard( name: 'Gravity Demon', title: 'The Abyssal Pull', - description: - 'A spirit of pure downward force that clings to the back of adventurers. ' - 'It whispers lies of weakness into your ear while dragging you towards the abyss.\n\n' - 'Only those with a back of steel and the will to pull themselves up can escape its grasp.', + description: + 'A spirit of pure downward force that clings to the back of adventurers. ' + 'It whispers lies of weakness into your ear while dragging you towards the abyss.\n\n' + 'Only those with a back of steel and the will to pull themselves up can escape its grasp.', assetPath: AssetPaths.enemyGravityDemon, exercise: 'Pull-up Nemesis', color: Colors.purpleAccent, @@ -46,10 +46,10 @@ class CodexScreen extends StatelessWidget { _LoreCard( name: 'Pressure Phantom', title: 'The Invisible Crusher', - description: - 'An ethereal entity that compresses the very air around you. ' - 'It seeks to collapse the chest and shoulders of any who dare to push against it.\n\n' - 'Defeat it by pushing through the pain with explosive dipping power.', + description: + 'An ethereal entity that compresses the very air around you. ' + 'It seeks to collapse the chest and shoulders of any who dare to push against it.\n\n' + 'Defeat it by pushing through the pain with explosive dipping power.', assetPath: AssetPaths.enemyPressurePhantom, exercise: 'Dip Nemesis', color: Colors.cyanAccent, @@ -97,14 +97,13 @@ class _LoreCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Header Image Container( height: 200, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.black26, image: DecorationImage( - image: AssetImage(AssetPaths.bgUndergroundGym), // Hintergrund für Atmosphäre + image: AssetImage(AssetPaths.bgUndergroundGym), fit: BoxFit.cover, opacity: 0.3, ), @@ -118,8 +117,6 @@ class _LoreCard extends StatelessWidget { ), ), ), - - // Content Padding( padding: const EdgeInsets.all(16), child: Column( @@ -130,14 +127,17 @@ class _LoreCard extends StatelessWidget { children: [ Text( name.toUpperCase(), - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - color: color, - fontWeight: FontWeight.bold, - letterSpacing: 1.5, - ), + style: + Theme.of(context).textTheme.headlineSmall?.copyWith( + color: color, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), ), Chip( - label: Text(exercise, style: const TextStyle(fontSize: 10, color: Colors.white)), + label: Text(exercise, + style: const TextStyle( + fontSize: 10, color: Colors.white)), backgroundColor: Colors.black54, padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, @@ -155,9 +155,9 @@ class _LoreCard extends StatelessWidget { Text( description, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - height: 1.5, - color: Colors.white70, - ), + height: 1.5, + color: Colors.white70, + ), ), ], ), diff --git a/lib/src/features/gamification/presentation/widgets/avatar_editor.dart b/lib/src/features/gamification/presentation/widgets/avatar_editor.dart index 6c926af..2e271f1 100644 --- a/lib/src/features/gamification/presentation/widgets/avatar_editor.dart +++ b/lib/src/features/gamification/presentation/widgets/avatar_editor.dart @@ -1,222 +1,3 @@ -// import 'package:flutter/material.dart'; -// import '../../../../core/theme/app_theme.dart'; -// import '../../../../core/constants/asset_paths.dart'; -// import '../../domain/entities/avatar_config.dart'; -// import 'avatar_renderer.dart'; - -// class AvatarEditor extends StatefulWidget { -// final AvatarConfig initialConfig; -// final ValueChanged onChanged; - -// const AvatarEditor({ -// super.key, -// required this.initialConfig, -// required this.onChanged, -// }); - -// @override -// State createState() => _AvatarEditorState(); -// } - -// class _AvatarEditorState extends State { -// late AvatarConfig _config; -// String _selectedTab = 'Body'; // Body, Hair, Style - -// @override -// void initState() { -// super.initState(); -// _config = widget.initialConfig; -// } - -// void _updateConfig(AvatarConfig newConfig) { -// setState(() => _config = newConfig); -// widget.onChanged(newConfig); -// } - -// @override -// Widget build(BuildContext context) { -// return Column( -// children: [ -// // 1. Live Preview -// Container( -// height: 220, -// alignment: Alignment.center, -// decoration: BoxDecoration( -// color: AppTheme.surfaceColor, -// border: const Border(bottom: BorderSide(color: Colors.white10)), -// ), -// child: AvatarRenderer(config: _config, size: 180), -// ), - -// // 2. Tabs -// Row( -// children: [ -// _buildTab('Body'), -// _buildTab('Hair'), -// _buildTab('Style'), -// ], -// ), - -// // 3. Options Grid -// Expanded( -// child: Container( -// color: AppTheme.backgroundColor, -// child: _buildOptionsGrid(), -// ), -// ), -// ], -// ); -// } - -// Widget _buildTab(String label) { -// final isSelected = _selectedTab == label; -// return Expanded( -// child: GestureDetector( -// onTap: () => setState(() => _selectedTab = label), -// child: Container( -// padding: const EdgeInsets.symmetric(vertical: 16), -// decoration: BoxDecoration( -// border: Border( -// bottom: BorderSide( -// color: isSelected ? AppTheme.primaryColor : Colors.transparent, -// width: 2, -// ), -// ), -// ), -// child: Text( -// label, -// textAlign: TextAlign.center, -// style: TextStyle( -// color: isSelected ? AppTheme.primaryColor : Colors.grey, -// fontWeight: FontWeight.bold, -// ), -// ), -// ), -// ), -// ); -// } - -// Widget _buildOptionsGrid() { -// switch (_selectedTab) { -// case 'Body': -// return ListView( -// padding: const EdgeInsets.all(16), -// children: [ -// const Text('Gender', style: TextStyle(color: Colors.grey)), -// const SizedBox(height: 8), -// Row( -// children: [ -// _buildChip('Male', _config.gender == 'male', () { -// _updateConfig(AvatarConfig( -// gender: 'male', skinTone: _config.skinTone, -// hairStyle: _config.hairStyle, clothing: _config.clothing)); -// }), -// const SizedBox(width: 8), -// _buildChip('Female', _config.gender == 'female', () { -// _updateConfig(AvatarConfig( -// gender: 'female', skinTone: _config.skinTone, -// hairStyle: _config.hairStyle, clothing: _config.clothing)); -// }), -// ], -// ), -// const SizedBox(height: 24), -// const Text('Skin Tone', style: TextStyle(color: Colors.grey)), -// const SizedBox(height: 12), -// Wrap( -// spacing: 12, -// runSpacing: 12, -// children: AvatarConstants.skinTones.entries.map((e) { -// return GestureDetector( -// onTap: () { -// _updateConfig(AvatarConfig( -// gender: _config.gender, skinTone: e.key, -// hairStyle: _config.hairStyle, clothing: _config.clothing)); -// }, -// child: Container( -// width: 48, -// height: 48, -// decoration: BoxDecoration( -// color: e.value, -// shape: BoxShape.circle, -// border: Border.all( -// color: _config.skinTone == e.key ? AppTheme.primaryColor : Colors.transparent, -// width: 3, -// ), -// ), -// ), -// ); -// }).toList(), -// ), -// ], -// ); -// case 'Hair': -// return _buildGrid(AvatarConstants.hairStyles.keys.toList(), (key) { -// _updateConfig(AvatarConfig( -// gender: _config.gender, skinTone: _config.skinTone, -// hairStyle: key, clothing: _config.clothing)); -// }, _config.hairStyle); -// case 'Style': -// return _buildGrid(AvatarConstants.clothing.keys.toList(), (key) { -// _updateConfig(AvatarConfig( -// gender: _config.gender, skinTone: _config.skinTone, -// hairStyle: _config.hairStyle, clothing: key)); -// }, _config.clothing); -// default: -// return const SizedBox(); -// } -// } - -// Widget _buildGrid(List items, Function(String) onSelect, String current) { -// return GridView.builder( -// padding: const EdgeInsets.all(16), -// gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( -// crossAxisCount: 3, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 1.0), -// itemCount: items.length, -// itemBuilder: (context, index) { -// final key = items[index]; -// final isSelected = current == key; -// return GestureDetector( -// onTap: () => onSelect(key), -// child: Container( -// decoration: BoxDecoration( -// color: AppTheme.surfaceColor, -// borderRadius: BorderRadius.circular(12), -// border: Border.all( -// color: isSelected ? AppTheme.primaryColor : Colors.transparent, -// width: 2, -// ), -// ), -// child: Center( -// // Für MVP zeigen wir den Text-Key, später könnte hier ein Icon hin -// child: Text( -// key.replaceAll('_', ' ').toUpperCase(), -// textAlign: TextAlign.center, -// style: TextStyle( -// color: isSelected ? AppTheme.primaryColor : Colors.grey, -// fontSize: 10, -// fontWeight: FontWeight.bold -// ), -// ), -// ), -// ), -// ); -// }, -// ); -// } - -// Widget _buildChip(String label, bool isSelected, VoidCallback onTap) { -// return ActionChip( -// label: Text(label), -// backgroundColor: isSelected ? AppTheme.primaryColor.withOpacity(0.2) : null, -// side: BorderSide(color: isSelected ? AppTheme.primaryColor : Colors.grey), -// labelStyle: TextStyle( -// color: isSelected ? AppTheme.primaryColor : Colors.white, -// fontWeight: FontWeight.bold, -// ), -// onPressed: onTap, -// ); -// } -// } import 'package:flutter/material.dart'; import '../../../../core/theme/app_theme.dart'; import '../../domain/entities/avatar_config.dart'; @@ -259,7 +40,6 @@ class _AvatarEditorState extends State { Widget build(BuildContext context) { return Column( children: [ - // Preview Container( height: 200, alignment: Alignment.center, @@ -269,8 +49,6 @@ class _AvatarEditorState extends State { size: 160, ), ), - - // Gender Switch Padding( padding: const EdgeInsets.all(16), child: SegmentedButton( @@ -280,8 +58,7 @@ class _AvatarEditorState extends State { ], selected: {_gender}, onSelectionChanged: (Set newSelection) { - _update( - newSelection.first, 1); // Reset to variant 1 on gender switch + _update(newSelection.first, 1); }, style: ButtonStyle( backgroundColor: MaterialStateProperty.resolveWith( @@ -291,8 +68,6 @@ class _AvatarEditorState extends State { ), ), ), - - // Variants Grid Expanded( child: GridView.builder( padding: const EdgeInsets.all(16), @@ -301,7 +76,7 @@ class _AvatarEditorState extends State { crossAxisSpacing: 12, mainAxisSpacing: 12, ), - itemCount: 8, // Wir haben 8 Varianten pro Sheet + itemCount: 8, itemBuilder: (context, index) { final variantNum = index + 1; final isSelected = _variant == variantNum; diff --git a/lib/src/features/gamification/presentation/widgets/avatar_renderer.dart b/lib/src/features/gamification/presentation/widgets/avatar_renderer.dart index d06f584..7bd9aa8 100644 --- a/lib/src/features/gamification/presentation/widgets/avatar_renderer.dart +++ b/lib/src/features/gamification/presentation/widgets/avatar_renderer.dart @@ -1,89 +1,3 @@ -// import 'package:flutter/material.dart'; -// import '../../domain/entities/avatar_config.dart'; - -// class AvatarRenderer extends StatelessWidget { -// final AvatarConfig config; -// final double size; - -// const AvatarRenderer({ -// super.key, -// required this.config, -// this.size = 120.0, -// }); - -// @override -// Widget build(BuildContext context) { -// return SizedBox( -// width: size, -// height: size, -// child: Stack( -// alignment: Alignment.center, -// children: [ -// // 1. Layer: Background Circle (Optional, for glow) -// Container( -// decoration: BoxDecoration( -// shape: BoxShape.circle, -// boxShadow: [ -// BoxShadow( -// color: Colors.black.withOpacity(0.3), -// blurRadius: 10, -// spreadRadius: 2, -// ), -// ], -// ), -// ), - -// // 2. Layer: Body Base (Tinted with Skin Tone) -// _buildLayer( -// assetPath: config.baseAsset, -// color: config.skinColor, -// blendMode: BlendMode.modulate, // Färbt das weiße Pixel-Art ein -// ), - -// // 3. Layer: Eyes/Face (Könnte separat sein, hier Teil der Base angenommen oder weggelassen) - -// // 4. Layer: Clothing -// if (config.clothingAsset != null) -// _buildLayer(assetPath: config.clothingAsset!), - -// // 5. Layer: Hair -// if (config.hairAsset != null) -// _buildLayer(assetPath: config.hairAsset!), -// ], -// ), -// ); -// } - -// Widget _buildLayer({ -// required String assetPath, -// Color? color, -// BlendMode? blendMode, -// }) { -// return Image.asset( -// assetPath, -// width: size, -// height: size, -// fit: BoxFit.contain, -// color: color, -// colorBlendMode: blendMode, -// // Fallback, falls Assets fehlen (damit die App nicht abstürzt) -// errorBuilder: (context, error, stackTrace) { -// return Container( -// width: size, -// height: size, -// decoration: BoxDecoration( -// color: Colors.grey.withOpacity(0.2), -// shape: BoxShape.circle, -// border: Border.all(color: Colors.red.withOpacity(0.3)), -// ), -// child: const Center( -// child: Icon(Icons.broken_image, size: 20, color: Colors.white24), -// ), -// ); -// }, -// ); -// } -// } import 'package:flutter/material.dart'; import '../../domain/entities/avatar_config.dart'; diff --git a/lib/src/features/history/presentation/screens/history_screen.dart b/lib/src/features/history/presentation/screens/history_screen.dart index 20425b5..47326e8 100644 --- a/lib/src/features/history/presentation/screens/history_screen.dart +++ b/lib/src/features/history/presentation/screens/history_screen.dart @@ -1,137 +1,3 @@ -// import 'package:flutter/material.dart'; -// import 'package:flutter_riverpod/flutter_riverpod.dart'; -// import 'package:go_router/go_router.dart'; -// import 'package:intl/intl.dart'; - -// import '../../../../core/theme/app_theme.dart'; -// import '../../../../shared/data/repositories/workout_repository.dart'; -// import '../../../../shared/data/local/collections/workout_collection.dart'; - -// class HistoryScreen extends ConsumerStatefulWidget { -// const HistoryScreen({super.key}); - -// @override -// ConsumerState createState() => _HistoryScreenState(); -// } - -// class _HistoryScreenState extends ConsumerState { -// @override -// Widget build(BuildContext context) { -// final workoutRepo = ref.watch(workoutRepositoryProvider); - -// return Scaffold( -// appBar: AppBar( -// title: const Text('Workout History'), -// leading: IconButton( -// icon: const Icon(Icons.arrow_back), -// onPressed: () => context.go('/hub'), -// ), -// ), -// body: FutureBuilder>( -// future: workoutRepo.getCompletedWorkouts(), -// builder: (context, snapshot) { -// if (snapshot.connectionState == ConnectionState.waiting) { -// return const Center(child: CircularProgressIndicator()); -// } - -// if (!snapshot.hasData || snapshot.data!.isEmpty) { -// return Center( -// child: Column( -// mainAxisAlignment: MainAxisAlignment.center, -// children: [ -// Icon( -// Icons.history, -// size: 80, -// color: AppTheme.primaryColor.withOpacity(0.5), -// ), -// const SizedBox(height: 16), -// Text( -// 'No workout history yet', -// style: Theme.of(context).textTheme.headlineMedium, -// ), -// const SizedBox(height: 8), -// Text( -// 'Complete your first workout to see it here', -// style: Theme.of(context).textTheme.bodyMedium, -// ), -// ], -// ), -// ); -// } - -// // Sort by date descending -// final workouts = snapshot.data! -// ..sort((a, b) => b.completedAt!.compareTo(a.completedAt!)); - -// return ListView.builder( -// padding: const EdgeInsets.all(16), -// itemCount: workouts.length, -// itemBuilder: (context, index) { -// final workout = workouts[index]; -// final dateStr = DateFormat.yMMMd().format(workout.completedAt!); -// final timeStr = DateFormat.jm().format(workout.completedAt!); - -// return Card( -// margin: const EdgeInsets.only(bottom: 16), -// child: ExpansionTile( -// leading: Container( -// width: 48, -// height: 48, -// decoration: BoxDecoration( -// color: AppTheme.primaryColor.withOpacity(0.2), -// borderRadius: BorderRadius.circular(8), -// ), -// child: Center( -// child: Text( -// 'W${workout.week}\nD${workout.day}', -// textAlign: TextAlign.center, -// style: const TextStyle( -// fontWeight: FontWeight.bold, -// fontSize: 12, -// ), -// ), -// ), -// ), -// title: Text( -// dateStr, -// style: Theme.of(context).textTheme.titleMedium?.copyWith( -// color: AppTheme.primaryColor, -// ), -// ), -// subtitle: Text('$timeStr • ${workout.xpEarned} XP'), -// children: [ -// Padding( -// padding: const EdgeInsets.all(16), -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// if (workout.notes.isNotEmpty) ...[ -// Text( -// 'Notes:', -// style: Theme.of(context).textTheme.labelLarge, -// ), -// Text(workout.notes), -// const SizedBox(height: 16), -// ], -// // We could parse exercisesJson here to show details -// // For MVP, just showing basic completion info -// Text( -// 'Workout Completed', -// style: TextStyle(color: AppTheme.successColor), -// ), -// ], -// ), -// ), -// ], -// ), -// ); -// }, -// ); -// }, -// ), -// ); -// } -// } import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -170,15 +36,13 @@ class _HistoryScreenState extends ConsumerState { return Scaffold( appBar: AppBar( - title: const Text( - 'Quest Log'), // "Quest Log" passt besser zum RPG Theme als "History" + title: const Text('Quest Log'), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/hub'), ), ), body: FutureBuilder>( - // future: workoutRepo.getCompletedWorkouts(), future: _loadHistory(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { @@ -210,7 +74,6 @@ class _HistoryScreenState extends ConsumerState { ); } - // Sort by date descending (newest first) final workouts = snapshot.data! ..sort((a, b) => b.completedAt!.compareTo(a.completedAt!)); @@ -249,7 +112,6 @@ class _WorkoutHistoryCard extends StatelessWidget { final timeStr = DateFormat.jm().format(workout.completedAt!); final exercises = _parseExercises(); - // Zusammenfassung der trainierten Muskelgruppen/Übungen für den Titel final summary = exercises .map((e) => e.exerciseName.replaceAll('Weighted ', '').replaceAll('Back ', '')) diff --git a/lib/src/features/inventory/presentation/widgets/plate_counter.dart b/lib/src/features/inventory/presentation/widgets/plate_counter.dart index 1b8e941..63e44c3 100644 --- a/lib/src/features/inventory/presentation/widgets/plate_counter.dart +++ b/lib/src/features/inventory/presentation/widgets/plate_counter.dart @@ -27,7 +27,6 @@ class PlateCounter extends StatelessWidget { padding: const EdgeInsets.all(12), child: Row( children: [ - // Plate Visual Container( width: 48, height: 48, @@ -53,16 +52,12 @@ class PlateCounter extends StatelessWidget { ), ), const SizedBox(width: 16), - - // Weight Label Expanded( child: Text( '${weight.toStringAsFixed(weight == weight.toInt() ? 0 : 2)} kg', style: Theme.of(context).textTheme.bodyLarge, ), ), - - // Counter IconButton( icon: const Icon(Icons.remove_circle_outline), onPressed: count > 0 ? () => onChanged(count - 1) : null, @@ -73,7 +68,7 @@ class PlateCounter extends StatelessWidget { height: 40, alignment: Alignment.center, decoration: BoxDecoration( - color: AppTheme.primaryColor.withOpacity(0.1), // Hintergrund + color: AppTheme.primaryColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all(color: AppTheme.primaryColor.withOpacity(0.3)), @@ -81,25 +76,15 @@ class PlateCounter extends StatelessWidget { child: Text( count.toString(), style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Colors.white, // Explizit Weiß + color: Colors.white, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), - // SizedBox( - // width: 32, - // child: Text( - // count.toString(), - // style: Theme.of(context).textTheme.titleLarge, - // textAlign: TextAlign.center, - // ), - // ), IconButton( icon: const Icon(Icons.add_circle_outline), - onPressed: count < 20 - ? () => onChanged(count + 1) - : null, // Limit erhöht auf 20 + onPressed: count < 20 ? () => onChanged(count + 1) : null, color: AppTheme.primaryColor, ), ], diff --git a/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart b/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart index efe1dc5..306701a 100644 --- a/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart @@ -9,7 +9,7 @@ import '../../../../shared/data/repositories/user_repository.dart'; import '../../../../shared/data/repositories/cycle_repository.dart'; import '../../../gamification/domain/entities/avatar_config.dart'; import '../../../gamification/presentation/widgets/avatar_editor.dart'; -import 'bodyweight_input_screen.dart'; // Für den Provider +import 'bodyweight_input_screen.dart'; class AvatarSetupScreen extends ConsumerStatefulWidget { const AvatarSetupScreen({super.key}); @@ -22,64 +22,6 @@ class _AvatarSetupScreenState extends ConsumerState { AvatarConfig _config = const AvatarConfig(); bool _isLoading = false; - // Future _handleFinish() async { - // setState(() => _isLoading = true); - - // try { - // final onboardingData = ref.read(onboardingDataProvider); - // final userRepo = ref.read(userRepositoryProvider); - - // // Inventory Settings aus dem Provider holen (muss dort gespeichert worden sein) - // final inventorySettings = onboardingData['inventory_settings'] as Map; - - // // Registrierung durchführen - // final user = await userRepo.register( - // email: onboardingData['email'] ?? '', - // password: onboardingData['password'] ?? '', - // bodyweight: onboardingData['bodyweight'] ?? 80.0, - // inventorySettings: inventorySettings, - // ); - - // // Avatar speichern (separates Update, da register meist nur Basisdaten nimmt, - // // oder wir packen es direkt in register rein, wenn die API es erlaubt. - // // Hier machen wir es sicherheitshalber als Update, falls register streng ist). - // // Update: UserRepo.register unterstützt avatarConfig laut Code! - // // Aber wir haben UserRepo.register schon aufgerufen. Da wir den User jetzt lokal haben, - // // können wir das avatarConfigJson updaten und speichern. - - // // Update local user with avatar config - // user.avatarConfigJson = jsonEncode(_config.toJson()); - // user.isDirty = true; - // await userRepo.saveLocalUser(user); - // // Optional: Sofort syncen, oder einfach auf Background Sync warten. - - // // Cycle erstellen - // final trainingMaxes = onboardingData['training_maxes'] as Map?; - // if (trainingMaxes != null) { - // final cycleRepo = ref.read(cycleRepositoryProvider); - // final tmMap = { - // 'squat': (trainingMaxes['squat'] as num?)?.toDouble() ?? 100.0, - // 'pullup': (trainingMaxes['pullup'] as num?)?.toDouble() ?? 80.0, - // 'dip': (trainingMaxes['dip'] as num?)?.toDouble() ?? 90.0, - // }; - // await cycleRepo.createCycle(tmMap); - // } - - // if (mounted) { - // ref.read(onboardingDataProvider.notifier).state = {}; // Cleanup - // context.go('/hub'); - // } - // } catch (e) { - // if (mounted) { - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text('Setup failed: $e'), backgroundColor: AppTheme.errorColor), - // ); - // } - // } finally { - // if (mounted) setState(() => _isLoading = false); - // } - // } - Future _handleFinish() async { setState(() => _isLoading = true); @@ -89,11 +31,9 @@ class _AvatarSetupScreenState extends ConsumerState { final inventorySettings = onboardingData['inventory_settings'] as Map; - // PRÜFUNG: Sind wir schon eingeloggt? (Reset Fall) var user = await userRepo.getLocalUser(); if (user == null) { - // FALL A: Neuer User -> Registrieren user = await userRepo.register( email: onboardingData['email'] ?? '', password: onboardingData['password'] ?? '', @@ -101,14 +41,12 @@ class _AvatarSetupScreenState extends ConsumerState { inventorySettings: inventorySettings, ); } else { - // FALL B: Existierender User (Reset) -> Nur Updaten user.currentBodyweight = onboardingData['bodyweight'] ?? user.currentBodyweight; user.inventorySettingsJson = jsonEncode(inventorySettings); user.isDirty = true; await userRepo.saveLocalUser(user); - // Server Update triggern (via Repo Methoden die API rufen) try { await userRepo.updateBodyweight(user.currentBodyweight); await userRepo.updateInventory(inventorySettings); @@ -117,12 +55,10 @@ class _AvatarSetupScreenState extends ConsumerState { } } - // Avatar speichern (für beide Fälle gleich) user!.avatarConfigJson = jsonEncode(_config.toJson()); user.isDirty = true; await userRepo.saveLocalUser(user); - // Cycle erstellen (für beide Fälle gleich) final trainingMaxes = onboardingData['training_maxes'] as Map?; if (trainingMaxes != null) { @@ -156,8 +92,7 @@ class _AvatarSetupScreenState extends ConsumerState { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - // title: const Text('Create Character'), - title: const Text('Choose Your Hero'), // Statt "Create Character" + title: const Text('Choose Your Hero'), actions: [ TextButton( onPressed: _isLoading ? null : _handleFinish, @@ -193,10 +128,6 @@ class _AvatarSetupScreenState extends ConsumerState { ), ], ), - // body: AvatarEditor( - // initialConfig: _config, - // onChanged: (newConfig) => _config = newConfig, - // ), ); } } 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 a6a1065..964dc1d 100644 --- a/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart @@ -30,7 +30,6 @@ class _InventorySetupScreenState extends ConsumerState { 1.25: 2, }; - // Band selection state - Default configuration based on standard colors final Map _bandInventory = { 'Blue': true, 'Green': true, @@ -81,7 +80,6 @@ class _InventorySetupScreenState extends ConsumerState { } void _handleNext() { - // Listen bauen (wie vorher) final platesList = []; _plateInventory.forEach((weight, count) { for (int i = 0; i < count; i++) platesList.add(weight); @@ -104,13 +102,12 @@ class _InventorySetupScreenState extends ConsumerState { 'bands': bandsList, }; - // Im Provider speichern für den nächsten Screen ref.read(onboardingDataProvider.notifier).update((state) => { ...state, 'inventory_settings': inventorySettings, }); - context.push('/onboarding/avatar'); // Neue Route! + context.push('/onboarding/avatar'); } Future _handleFinish() async { @@ -120,7 +117,6 @@ class _InventorySetupScreenState extends ConsumerState { final onboardingData = ref.read(onboardingDataProvider); final userRepo = ref.read(userRepositoryProvider); - // Build plates list for DB final platesList = []; _plateInventory.forEach((weight, count) { for (int i = 0; i < count; i++) { @@ -128,7 +124,6 @@ class _InventorySetupScreenState extends ConsumerState { } }); - // Build bands list for DB final bandsList = >[]; _bandInventory.forEach((color, isSelected) { if (isSelected) { @@ -146,7 +141,6 @@ class _InventorySetupScreenState extends ConsumerState { 'bands': bandsList, }; - // Register user with all data final user = await userRepo.register( email: onboardingData['email'] ?? '', password: onboardingData['password'] ?? '', @@ -156,7 +150,6 @@ class _InventorySetupScreenState extends ConsumerState { debugPrint('✅ User registered: ${user.serverId}'); - // Create first cycle final trainingMaxes = onboardingData['training_maxes'] as Map?; @@ -174,10 +167,8 @@ class _InventorySetupScreenState extends ConsumerState { } if (mounted) { - // Clear onboarding data ref.read(onboardingDataProvider.notifier).state = {}; - // Navigate to hub context.go('/hub'); } } catch (e, stackTrace) { @@ -186,7 +177,6 @@ class _InventorySetupScreenState extends ConsumerState { if (mounted) { String message = 'Setup failed: ${e.toString()}'; - // Catch unique constraint error (PocketBase returns 400 usually) if (e.toString().toLowerCase().contains('unique') || e.toString().toLowerCase().contains('email')) { message = 'Email already exists. Please login or use another email.'; @@ -238,15 +228,12 @@ class _InventorySetupScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Progress Indicator LinearProgressIndicator( value: 0.75, backgroundColor: AppTheme.xpBarBackground, color: AppTheme.primaryColor, ), const SizedBox(height: 32), - - // Title Text( 'Equipment Inventory', style: Theme.of(context).textTheme.displayMedium, @@ -257,8 +244,6 @@ class _InventorySetupScreenState extends ConsumerState { style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 32), - - // Bar Weight Selector Text( 'Barbell Weight', style: Theme.of(context) @@ -286,8 +271,6 @@ class _InventorySetupScreenState extends ConsumerState { textAlign: TextAlign.center, ), const SizedBox(height: 32), - - // Presets Text( 'Quick Presets', style: Theme.of(context) @@ -321,8 +304,6 @@ class _InventorySetupScreenState extends ConsumerState { ], ), const SizedBox(height: 32), - - // Plate Selection Text( 'Available Plates', style: Theme.of(context) @@ -342,9 +323,7 @@ class _InventorySetupScreenState extends ConsumerState { }, ); }).toList(), - const SizedBox(height: 32), - Text( 'Resistance Bands (Assistance)', style: Theme.of(context) @@ -385,27 +364,11 @@ class _InventorySetupScreenState extends ConsumerState { ); }).toList(), ), - const SizedBox(height: 32), - ElevatedButton( onPressed: _handleNext, child: const Text('NEXT STEP'), ), - // // Finish Button - // ElevatedButton( - // onPressed: _isLoading ? null : _handleFinish, - // child: _isLoading - // ? const SizedBox( - // height: 20, - // width: 20, - // child: CircularProgressIndicator( - // strokeWidth: 2, - // color: Colors.black, - // ), - // ) - // : const Text('FINISH SETUP'), - // ), ], ), ), @@ -413,88 +376,3 @@ class _InventorySetupScreenState extends ConsumerState { ); } } - -// class _PlateCounter extends StatelessWidget { -// final double weight; -// final int count; -// final ValueChanged onChanged; - -// const _PlateCounter({ -// required this.weight, -// required this.count, -// required this.onChanged, -// }); - -// Color _getPlateColor() { -// final colorValue = PlateColors.colors[weight]; -// return colorValue != null ? Color(colorValue) : Colors.grey; -// } - -// @override -// Widget build(BuildContext context) { -// return Card( -// margin: const EdgeInsets.only(bottom: 8), -// child: Padding( -// padding: const EdgeInsets.all(12), -// child: Row( -// children: [ -// // Plate Visual -// Container( -// width: 48, -// height: 48, -// decoration: BoxDecoration( -// color: _getPlateColor(), -// shape: BoxShape.circle, -// border: Border.all( -// color: Colors.white24, -// width: 2, -// ), -// ), -// child: Center( -// child: Text( -// weight == weight.toInt() -// ? '${weight.toInt()}' -// : weight.toStringAsFixed(2), -// style: const TextStyle( -// color: Colors.white, -// fontWeight: FontWeight.bold, -// fontSize: 12, -// ), -// ), -// ), -// ), -// const SizedBox(width: 16), - -// // Weight Label -// Expanded( -// child: Text( -// '${weight.toStringAsFixed(weight == weight.toInt() ? 0 : 2)} kg', -// style: Theme.of(context).textTheme.bodyLarge, -// ), -// ), - -// // Counter -// IconButton( -// icon: const Icon(Icons.remove_circle_outline), -// onPressed: count > 0 ? () => onChanged(count - 1) : null, -// color: AppTheme.primaryColor, -// ), -// SizedBox( -// width: 32, -// child: Text( -// count.toString(), -// style: Theme.of(context).textTheme.titleLarge, -// textAlign: TextAlign.center, -// ), -// ), -// IconButton( -// icon: const Icon(Icons.add_circle_outline), -// onPressed: count < 10 ? () => onChanged(count + 1) : null, -// color: AppTheme.primaryColor, -// ), -// ], -// ), -// ), -// ); -// } -// } 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 8e451ca..cf1c47e 100644 --- a/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/strength_test_screen.dart @@ -17,7 +17,6 @@ class StrengthTestScreen extends ConsumerStatefulWidget { class _StrengthTestScreenState extends ConsumerState { final _formKey = GlobalKey(); - // Controllers for each exercise final _squatWeightController = TextEditingController(text: '100'); final _squatRepsController = TextEditingController(text: '5'); final _pullupWeightController = TextEditingController(text: '0'); @@ -48,20 +47,17 @@ class _StrengthTestScreenState extends ConsumerState { void _calculateAll() { final bodyweight = ref.read(onboardingDataProvider)['bodyweight'] ?? 80.0; - // Squat final squatWeight = double.tryParse(_squatWeightController.text) ?? 0; final squatReps = int.tryParse(_squatRepsController.text) ?? 1; final squat1RM = WendlerCalculator.calculate1RM(squatWeight, squatReps); final squatTM = WendlerCalculator.calculateTrainingMax(squat1RM); - // Pullup (bodyweight + additional weight) final pullupAdditional = double.tryParse(_pullupWeightController.text) ?? 0; final pullupReps = int.tryParse(_pullupRepsController.text) ?? 1; final pullupTotal = bodyweight + pullupAdditional; final pullup1RM = WendlerCalculator.calculate1RM(pullupTotal, pullupReps); final pullupTM = WendlerCalculator.calculateTrainingMax(pullup1RM); - // Dip (bodyweight + additional weight) final dipAdditional = double.tryParse(_dipWeightController.text) ?? 0; final dipReps = int.tryParse(_dipRepsController.text) ?? 1; final dipTotal = bodyweight + dipAdditional; @@ -85,7 +81,6 @@ class _StrengthTestScreenState extends ConsumerState { void _handleContinue() { if (!_formKey.currentState!.validate()) return; - // Store training maxes ref.read(onboardingDataProvider.notifier).update((state) => { ...state, 'training_maxes': _calculatedTMs, @@ -114,17 +109,14 @@ class _StrengthTestScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Progress Indicator LinearProgressIndicator( value: 0.5, backgroundColor: AppTheme.xpBarBackground, color: AppTheme.primaryColor, ), const SizedBox(height: 32), - - // Title Text( - 'Combat Calibration', // Statt "Strength Assessment" + 'Combat Calibration', style: Theme.of(context).textTheme.displayMedium, ), const SizedBox(height: 8), @@ -132,18 +124,12 @@ class _StrengthTestScreenState extends ConsumerState { 'We need to assess your current power level to assign the correct monsters.', // Flavor style: Theme.of(context).textTheme.bodyMedium, ), - // Text( - // 'Strength Assessment', - // style: Theme.of(context).textTheme.displayMedium, - // ), const SizedBox(height: 8), Text( 'Enter your recent best performance for each exercise', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 32), - - // Squat _ExerciseCard( exerciseName: 'Back Squat', icon: Icons.accessibility_new, @@ -155,8 +141,6 @@ class _StrengthTestScreenState extends ConsumerState { onChanged: _calculateAll, ), const SizedBox(height: 16), - - // Pullup _ExerciseCard( exerciseName: 'Weighted Pull-up', icon: Icons.north, @@ -169,8 +153,6 @@ class _StrengthTestScreenState extends ConsumerState { onChanged: _calculateAll, ), const SizedBox(height: 16), - - // Dip _ExerciseCard( exerciseName: 'Weighted Dip', icon: Icons.south, @@ -183,8 +165,6 @@ class _StrengthTestScreenState extends ConsumerState { onChanged: _calculateAll, ), const SizedBox(height: 32), - - // Info Box Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -202,10 +182,6 @@ class _StrengthTestScreenState extends ConsumerState { ), const SizedBox(width: 12), Expanded( - // child: Text( - // 'Training Max (TM) = 90% of your estimated 1RM. This is what we\'ll use for programming.', - // style: Theme.of(context).textTheme.bodySmall, - // ), child: Text( 'Your "Training Max" (TM) is your base combat power. We calculate it as 90% of your max potential to ensure long-term survival.', // Flavor style: Theme.of(context) @@ -218,8 +194,6 @@ class _StrengthTestScreenState extends ConsumerState { ), ), const SizedBox(height: 32), - - // Continue Button ElevatedButton( onPressed: _handleContinue, child: const Text('CONTINUE'), @@ -285,7 +259,6 @@ class _ExerciseCard extends StatelessWidget { ), const SizedBox(height: 16), - // Input Fields Row( children: [ Expanded( @@ -341,7 +314,6 @@ class _ExerciseCard extends StatelessWidget { ), const SizedBox(height: 16), - // Calculations Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( diff --git a/lib/src/features/onboarding/presentation/screens/welcome_screen.dart b/lib/src/features/onboarding/presentation/screens/welcome_screen.dart index 645a9c7..e248da8 100644 --- a/lib/src/features/onboarding/presentation/screens/welcome_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/welcome_screen.dart @@ -7,115 +7,22 @@ import '../../../../core/constants/asset_paths.dart'; class WelcomeScreen extends StatelessWidget { const WelcomeScreen({super.key}); - // @override - // Widget build(BuildContext context) { - // return Scaffold( - // body: SafeArea( - // child: Padding( - // padding: const EdgeInsets.all(24), - // child: Column( - // mainAxisAlignment: MainAxisAlignment.center, - // crossAxisAlignment: CrossAxisAlignment.stretch, - // children: [ - // const Spacer(), - - // // Logo - // Container( - // width: 120, - // height: 120, - // decoration: BoxDecoration( - // color: AppTheme.primaryColor, - // borderRadius: BorderRadius.circular(24), - // ), - // child: const Icon( - // Icons.fitness_center, - // size: 64, - // color: Colors.black, - // ), - // ), - // const SizedBox(height: 32), - - // // Title - // Text( - // 'WELCOME TO', - // style: Theme.of(context).textTheme.headlineMedium, - // textAlign: TextAlign.center, - // ), - // Text( - // 'SLRPG', - // style: Theme.of(context).textTheme.displayLarge, - // textAlign: TextAlign.center, - // ), - // const SizedBox(height: 16), - - // // Description - // Text( - // 'Transform your training into an epic RPG adventure', - // style: Theme.of(context).textTheme.bodyLarge, - // textAlign: TextAlign.center, - // ), - // const SizedBox(height: 48), - - // // Features - // _FeatureItem( - // icon: Icons.trending_up, - // title: 'Progressive Overload', - // description: 'Wendler 5/3/1 periodization', - // ), - // const SizedBox(height: 16), - // _FeatureItem( - // icon: Icons.videogame_asset, - // title: 'Gamified Training', - // description: 'Level up, earn XP, unlock achievements', - // ), - // const SizedBox(height: 16), - // _FeatureItem( - // icon: Icons.offline_bolt, - // title: 'Offline First', - // description: 'Train anywhere, sync when ready', - // ), - - // const Spacer(), - - // // Continue Button - // ElevatedButton( - // onPressed: () => context.go('/onboarding/bodyweight'), - // child: const Text('GET STARTED'), - // ), - // const SizedBox(height: 16), - - // // Skip to Login - // TextButton( - // onPressed: () => context.go('/login'), - // child: const Text('Already have an account? Login'), - // ), - // ], - // ), - // ), - // ), - // ); - // } - // // ... imports @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ - // 1. Hintergrund (Street Park) Positioned.fill( child: Image.asset( AssetPaths.bgStreetParkDay, fit: BoxFit.cover, ), ), - // 2. Overlay (Dunkel für Lesbarkeit) Positioned.fill( child: Container( color: Colors.black.withOpacity(0.7), ), ), - - // 3. Inhalt SafeArea( child: Padding( padding: const EdgeInsets.all(24), @@ -124,8 +31,6 @@ class WelcomeScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Spacer(), - - // Logo (Optional: Kann bleiben oder weg) Container( width: 100, height: 100, @@ -142,10 +47,8 @@ class WelcomeScreen extends StatelessWidget { size: 56, color: Colors.black), ), const SizedBox(height: 32), - - // RPG Title Text( - 'ENTER THE ARENA', // Statt "WELCOME TO" + 'ENTER THE ARENA', style: Theme.of(context).textTheme.headlineMedium?.copyWith( color: Colors.white70, letterSpacing: 2, @@ -164,8 +67,6 @@ class WelcomeScreen extends StatelessWidget { textAlign: TextAlign.center, ), const SizedBox(height: 16), - - // RPG Description const Text( 'The Iron Golems have awakened. The Gravity Demons are pulling the world into the abyss.\n\n' 'Only a true Streetlifter can stop them. Are you ready to forge your body into a weapon?', @@ -174,32 +75,25 @@ class WelcomeScreen extends StatelessWidget { textAlign: TextAlign.center, ), const SizedBox(height: 48), - - // Features (Umformuliert) _FeatureItem( - icon: Icons.shield, // Statt trending_up + icon: Icons.shield, title: 'Build Your Armor', description: 'Progressive overload based on Wendler 5/3/1.', ), const SizedBox(height: 16), _FeatureItem( icon: Icons.videogame_asset, - // icon: Icons - // .swords, // Statt videogame_asset (wenn Icon verfügbar, sonst Flash/Star) title: 'Slay Monsters', description: 'Turn every rep into damage against epic foes.', ), const SizedBox(height: 16), _FeatureItem( - icon: Icons.inventory_2, // Statt offline_bolt + icon: Icons.inventory_2, title: 'Gather Loot', description: 'Earn XP, level up, and unlock new gear.', ), - const Spacer(), - - // Button ElevatedButton( onPressed: () => context.go('/onboarding/bodyweight'), style: ElevatedButton.styleFrom( @@ -211,7 +105,6 @@ class WelcomeScreen extends StatelessWidget { fontWeight: FontWeight.bold, letterSpacing: 1)), ), const SizedBox(height: 16), - TextButton( onPressed: () => context.go('/login'), child: const Text('Already a hero? Login here', diff --git a/lib/src/features/stats/presentation/screens/stats_screen.dart b/lib/src/features/stats/presentation/screens/stats_screen.dart index 96736dc..9fa6924 100644 --- a/lib/src/features/stats/presentation/screens/stats_screen.dart +++ b/lib/src/features/stats/presentation/screens/stats_screen.dart @@ -26,7 +26,6 @@ class _StatsScreenState extends ConsumerState { String _selectedExercise = 'squat'; // squat, pullup, dip String _selectedRange = '3m'; // 1m, 3m, 1y, all - // Daten List _chartData = []; bool _isChartLoading = true; @@ -49,7 +48,6 @@ class _StatsScreenState extends ConsumerState { return; } final userId = user.serverId ?? user.id.toString(); - // 1. Alle abgeschlossenen Workouts laden (Lokal aus Isar) final allWorkouts = await workoutRepo.getCompletedWorkouts(userId); final points = []; @@ -57,7 +55,6 @@ class _StatsScreenState extends ConsumerState { for (var workout in allWorkouts) { if (workout.completedAt == null) continue; - // 2. Exercises parsen List exercisesJson = []; try { exercisesJson = jsonDecode(workout.exercisesJson); @@ -68,35 +65,26 @@ class _StatsScreenState extends ConsumerState { double max1RM = 0.0; double sessionVolume = 0.0; bool foundExercise = false; - double trainingMax = - 0.0; // Versuchen wir aus den Daten zu raten oder nehmen 0 + double trainingMax = 0.0; - // 3. Durch Übungen iterieren for (var exJson in exercisesJson) { final exercise = Exercise.fromJson(exJson); - // Nur die ausgewählte Übung betrachten if (exercise.exerciseId == _selectedExercise) { foundExercise = true; for (var set in exercise.sets) { if (!set.completed || set.repsActual <= 0) continue; - // Volumen summieren sessionVolume += set.targetWeightTotal * set.repsActual; - // 1RM berechnen (Epley Formel) - // Wir nutzen den WendlerCalculator, der die Logik schon hat final e1rm = WendlerCalculator.calculate1RM( set.targetWeightTotal, set.repsActual); - // Wir nehmen das beste Set des Tages als Wert für den Graphen if (e1rm > max1RM) { max1RM = e1rm; } - // Versuchen, das TM aus dem Prozentsatz rückzurechnen (optional) - // TM = Weight / Percentage. if (set.targetPercentage > 0 && trainingMax == 0) { trainingMax = set.targetWeightTotal / (set.targetPercentage / 100.0); @@ -105,22 +93,18 @@ class _StatsScreenState extends ConsumerState { } } - // 4. Datenpunkt erstellen, wenn Übung in diesem Workout vorkam if (foundExercise && max1RM > 0) { points.add(StatsDataPoint( date: workout.completedAt!, - trainingMax: - trainingMax, // Ist ggf. ungenau durch Rückrechnung, aber für Graph ok + trainingMax: trainingMax, estimated1RM: max1RM, totalVolume: sessionVolume, )); } } - // 5. Sortieren & Filtern (Zeitraum) points.sort((a, b) => a.date.compareTo(b.date)); - // Filter nach Datum (Range) final now = DateTime.now(); final filteredPoints = points.where((p) { if (_selectedRange == '1m') { @@ -130,7 +114,7 @@ class _StatsScreenState extends ConsumerState { } else if (_selectedRange == '1y') { return p.date.isAfter(now.subtract(const Duration(days: 365))); } - return true; // 'all' + return true; }).toList(); if (mounted) { @@ -149,45 +133,6 @@ class _StatsScreenState extends ConsumerState { } } } - // Future _loadStats() async { - // setState(() => _isChartLoading = true); - // try { - // final apiClient = ref - // .read(apiClientProvider); // Braucht Provider in user_repository.dart - - // // Hier rufen wir die echte API auf - // // Hinweis: Wenn Offline, müssten wir hier lokal aus Isar aggregieren. - // // Für MVP nutzen wir den API Endpoint wie im TDD spezifiziert. - // try { - // final response = await apiClient.getStatsHistory( - // exercise: _selectedExercise, - // range: _selectedRange, - // ); - - // final pointsJson = response['data_points'] as List? ?? []; - // final points = - // pointsJson.map((json) => StatsDataPoint.fromJson(json)).toList(); - - // if (mounted) { - // setState(() { - // _chartData = points; - // _isChartLoading = false; - // }); - // } - // } catch (e) { - // // Fallback/Error Handling (z.B. Offline) - // debugPrint('Failed to load stats: $e'); - // if (mounted) { - // setState(() { - // _chartData = []; // Leer anzeigen - // _isChartLoading = false; - // }); - // } - // } - // } catch (e) { - // // ... - // } - // } void _onFilterChanged(String exercise, String range) { setState(() { @@ -202,17 +147,14 @@ class _StatsScreenState extends ConsumerState { try { final cycleRepo = ref.read(cycleRepositoryProvider); - // 1. Alte TMs merken für den Vergleich final oldTMs = jsonDecode(currentCycle.trainingMaxesJson) as Map; - // 2. Zyklus abschließen (Stall Handling Logic läuft hier) final newCycle = await cycleRepo.finishCycle(); final newTMs = jsonDecode(newCycle.trainingMaxesJson) as Map; if (mounted) { - // 3. Ergebnis anzeigen await showDialog( context: context, barrierDismissible: false, @@ -223,7 +165,6 @@ class _StatsScreenState extends ConsumerState { ), ); - // UI aktualisieren setState(() {}); } } catch (e) { @@ -265,7 +206,6 @@ class _StatsScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Cycle Card (bleibt wie vorher) if (currentCycle != null) ...[ _CurrentCycleCard( cycle: currentCycle, @@ -285,7 +225,6 @@ class _StatsScreenState extends ConsumerState { ), const SizedBox(height: 16), - // --- FILTER CHIPS --- SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -312,7 +251,6 @@ class _StatsScreenState extends ConsumerState { ), const SizedBox(height: 16), - // --- CHART --- _isChartLoading ? const SizedBox( height: 250, @@ -322,58 +260,6 @@ class _StatsScreenState extends ConsumerState { // (Optional: Range Selector unten drunter '1M', '3M', '1Y'...) ], ), - // return SingleChildScrollView( - // padding: const EdgeInsets.all(16), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.stretch, - // children: [ - // if (currentCycle != null) ...[ - // _CurrentCycleCard( - // cycle: currentCycle, - // onFinish: _isLoading - // ? null - // : () => _handleFinishCycle(currentCycle), - // ), - // const SizedBox(height: 24), - // ] else - // const Card( - // child: Padding( - // padding: EdgeInsets.all(16), - // child: Text('No active cycle found.'), - // ), - // ), - - // // Platzhalter für zukünftige Graphen - // Text( - // 'Progress Charts', - // style: Theme.of(context).textTheme.titleLarge, - // ), - // const SizedBox(height: 8), - // Container( - // height: 200, - // decoration: BoxDecoration( - // color: AppTheme.surfaceColor, - // borderRadius: BorderRadius.circular(12), - // border: Border.all(color: Colors.white10), - // ), - // child: Center( - // child: Column( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Icon(Icons.bar_chart, - // size: 48, - // color: AppTheme.primaryColor.withOpacity(0.5)), - // const SizedBox(height: 8), - // Text( - // 'Coming Soon', - // style: Theme.of(context).textTheme.bodyMedium, - // ), - // ], - // ), - // ), - // ), - // ], - // ), ); }, ), @@ -488,7 +374,6 @@ class _CycleFinishDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - // title: Text('Cycle $newCycleNumber Started!'), title: const Text('Dungeon Cleared!'), content: Column( mainAxisSize: MainAxisSize.min, @@ -501,8 +386,6 @@ class _CycleFinishDialog extends StatelessWidget { const SizedBox(height: 8), const Text('Your Training Maxes have increased:', style: TextStyle(fontWeight: FontWeight.bold)), - // const Text( - // 'Based on your performance in Week 3, your Training Maxes have been updated:'), const SizedBox(height: 16), _DiffRow( name: 'Squat', oldVal: oldTMs['squat'], newVal: newTMs['squat']), @@ -517,7 +400,6 @@ class _CycleFinishDialog extends StatelessWidget { ElevatedButton( onPressed: () => Navigator.of(context).pop(), child: const Text('ENTER NEXT LEVEL'), - // child: const Text('LET\'S GO!'), ), ], ); diff --git a/lib/src/features/stats/presentation/widgets/progress_chart.dart b/lib/src/features/stats/presentation/widgets/progress_chart.dart index a1cb2ff..223124d 100644 --- a/lib/src/features/stats/presentation/widgets/progress_chart.dart +++ b/lib/src/features/stats/presentation/widgets/progress_chart.dart @@ -1,174 +1,3 @@ -// import 'package:fl_chart/fl_chart.dart'; -// import 'package:flutter/material.dart'; -// import 'package:intl/intl.dart'; -// import '../../../../core/theme/app_theme.dart'; -// import '../../domain/entities/stats_data_point.dart'; - -// class ProgressChart extends StatelessWidget { -// final List data; -// final bool isEmpty; - -// const ProgressChart({ -// super.key, -// required this.data, -// }) : isEmpty = data.isEmpty; - -// @override -// Widget build(BuildContext context) { -// if (isEmpty) { -// return Container( -// height: 250, -// decoration: BoxDecoration( -// color: AppTheme.surfaceColor, -// borderRadius: BorderRadius.circular(16), -// ), -// child: Center( -// child: Text( -// 'No data available yet', -// style: TextStyle(color: AppTheme.textSecondary), -// ), -// ), -// ); -// } - -// // Daten sortieren -// final points = List.from(data) -// ..sort((a, b) => a.date.compareTo(b.date)); - -// // Min/Max für Y-Achse berechnen (mit etwas Puffer) -// double maxY = points.map((e) => e.estimated1RM).reduce((a, b) => a > b ? a : b); -// double minY = points.map((e) => e.estimated1RM).reduce((a, b) => a < b ? a : b); - -// // Puffer hinzufügen (z.B. +/- 5kg), damit die Linie nicht am Rand klebt -// maxY += 5; -// minY = (minY - 5).clamp(0, double.infinity); - -// return Container( -// height: 250, -// padding: const EdgeInsets.fromLTRB(16, 24, 16, 0), -// decoration: BoxDecoration( -// color: AppTheme.surfaceColor, -// borderRadius: BorderRadius.circular(16), -// border: Border.all(color: AppTheme.primaryColor.withOpacity(0.1)), -// ), -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.stretch, -// children: [ -// Text( -// 'Estimated 1RM Progress', -// style: Theme.of(context).textTheme.titleSmall?.copyWith( -// color: AppTheme.textSecondary, -// ), -// textAlign: TextAlign.center, -// ), -// const SizedBox(height: 24), -// Expanded( -// child: LineChart( -// LineChartData( -// gridData: FlGridData( -// show: true, -// drawVerticalLine: false, -// horizontalInterval: 5, -// getDrawingHorizontalLine: (value) => FlLine( -// color: Colors.white10, -// strokeWidth: 1, -// ), -// ), -// titlesData: FlTitlesData( -// show: true, -// topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), -// rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), -// bottomTitles: AxisTitles( -// sideTitles: SideTitles( -// showTitles: true, -// reservedSize: 30, -// interval: (points.length / 3).ceil().toDouble(), // Zeige nicht jedes Datum -// getTitlesWidget: (value, meta) { -// final index = value.toInt(); -// if (index >= 0 && index < points.length) { -// return Padding( -// padding: const EdgeInsets.only(top: 8.0), -// child: Text( -// DateFormat.Md().format(points[index].date), -// style: const TextStyle( -// color: Colors.grey, -// fontSize: 10, -// ), -// ), -// ); -// } -// return const Text(''); -// }, -// ), -// ), -// leftTitles: AxisTitles( -// sideTitles: SideTitles( -// showTitles: true, -// reservedSize: 40, -// interval: 5, // Alle 5kg eine Beschriftung -// getTitlesWidget: (value, meta) { -// return Text( -// value.toInt().toString(), -// style: const TextStyle( -// color: Colors.grey, -// fontSize: 10, -// ), -// ); -// }, -// ), -// ), -// ), -// borderData: FlBorderData(show: false), -// minX: 0, -// maxX: (points.length - 1).toDouble(), -// minY: minY, -// maxY: maxY, -// lineBarsData: [ -// LineChartBarData( -// spots: points.asMap().entries.map((e) { -// return FlSpot(e.key.toDouble(), e.value.estimated1RM); -// }).toList(), -// isCurved: true, // Kurve glätten -// color: AppTheme.primaryColor, -// barWidth: 3, -// isStrokeCapRound: true, -// dotData: FlDotData( -// show: true, -// getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter( -// radius: 4, -// color: AppTheme.backgroundColor, -// strokeWidth: 2, -// strokeColor: AppTheme.primaryColor, -// ), -// ), -// belowBarData: BarAreaData( -// show: true, -// color: AppTheme.primaryColor.withOpacity(0.1), -// ), -// ), -// ], -// lineTouchData: LineTouchData( -// touchTooltipData: LineTouchTooltipData( -// getTooltipColor: (touchedSpot) => AppTheme.surfaceColor, -// getTooltipItems: (touchedSpots) { -// return touchedSpots.map((spot) { -// final date = points[spot.x.toInt()].date; -// return LineTooltipItem( -// '${spot.y.toStringAsFixed(1)} kg\n${DateFormat.yMMMd().format(date)}', -// const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), -// ); -// }).toList(); -// }, -// ), -// ), -// ), -// ), -// ), -// ], -// ), -// ); -// } -// } import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -178,13 +7,11 @@ import '../../domain/entities/stats_data_point.dart'; class ProgressChart extends StatelessWidget { final List data; - // FIX 1: 'const' Konstruktor erlaubt, da wir keine Berechnung mehr hier machen const ProgressChart({ super.key, required this.data, }); - // FIX 1: isEmpty als Getter (wird bei Zugriff berechnet) bool get isEmpty => data.isEmpty; @override @@ -200,7 +27,6 @@ class ProgressChart extends StatelessWidget { child: Text( 'No data available yet', style: Theme.of(context).textTheme.bodyMedium?.copyWith( - // Kleine Anpassung für Typ-Sicherheit color: AppTheme.textSecondary, ), ), @@ -208,17 +34,14 @@ class ProgressChart extends StatelessWidget { ); } - // Daten sortieren final points = List.from(data) ..sort((a, b) => a.date.compareTo(b.date)); - // Min/Max für Y-Achse berechnen (mit etwas Puffer) double maxY = points.map((e) => e.estimated1RM).reduce((a, b) => a > b ? a : b); double minY = points.map((e) => e.estimated1RM).reduce((a, b) => a < b ? a : b); - // Puffer hinzufügen (z.B. +/- 5kg), damit die Linie nicht am Rand klebt maxY += 5; minY = (minY - 5).clamp(0, double.infinity); @@ -263,9 +86,7 @@ class ProgressChart extends StatelessWidget { sideTitles: SideTitles( showTitles: true, reservedSize: 30, - interval: (points.length / 3) - .ceil() - .toDouble(), // Zeige nicht jedes Datum + interval: (points.length / 3).ceil().toDouble(), getTitlesWidget: (value, meta) { final index = value.toInt(); if (index >= 0 && index < points.length) { @@ -288,7 +109,7 @@ class ProgressChart extends StatelessWidget { sideTitles: SideTitles( showTitles: true, reservedSize: 40, - interval: 5, // Alle 5kg eine Beschriftung + interval: 5, getTitlesWidget: (value, meta) { return Text( value.toInt().toString(), @@ -311,7 +132,7 @@ class ProgressChart extends StatelessWidget { spots: points.asMap().entries.map((e) { return FlSpot(e.key.toDouble(), e.value.estimated1RM); }).toList(), - isCurved: true, // Kurve glätten + isCurved: true, color: AppTheme.primaryColor, barWidth: 3, isStrokeCapRound: true, @@ -334,7 +155,8 @@ class ProgressChart extends StatelessWidget { lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( // FIX 2: Alte API nutzen (tooltipBgColor statt getTooltipColor) - tooltipBgColor: AppTheme.surfaceColor, + // tooltipBgColor: AppTheme.surfaceColor, + getTooltipColor: (touchedSpot) => AppTheme.surfaceColor, getTooltipItems: (touchedSpots) { return touchedSpots.map((spot) { final date = points[spot.x.toInt()].date; 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 d42c8ec..6709f59 100644 --- a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart +++ b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart @@ -58,7 +58,6 @@ class _BattleScreenState extends ConsumerState { } String _getEnemyAsset(String exerciseId) { - // Mapping basierend auf Übungs-ID switch (exerciseId) { case 'squat': return AssetPaths.enemyIronGolem; @@ -67,7 +66,7 @@ class _BattleScreenState extends ConsumerState { case 'dip': return AssetPaths.enemyPressurePhantom; default: - return AssetPaths.enemyIronGolem; // Fallback + return AssetPaths.enemyIronGolem; } } @@ -472,34 +471,25 @@ class _BattleScreenState extends ConsumerState { ), 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 + color: Colors.black.withOpacity(0.7), ), ), - - // 3. INHALT SafeArea( child: _isResting - ? _buildRestScreen() // Rest Screen überdeckt das Gym (oder man macht ihn auch transparent) + ? _buildRestScreen() : _buildWorkoutScreen(currentExercise, currentSet, plateResult, completedHP, totalHP), ), ], ), - // body: _isResting - // ? _buildRestScreen() - // : _buildWorkoutScreen( - // currentExercise, currentSet, plateResult, completedHP, totalHP), ); } @@ -568,7 +558,6 @@ class _BattleScreenState extends ConsumerState { int completedHP, int totalHP, ) { - // Styles final readableStyle = Theme.of(context).textTheme.bodyLarge?.copyWith( color: Colors.white, shadows: [ @@ -586,26 +575,20 @@ class _BattleScreenState extends ConsumerState { return Column( children: [ - // --- 1. GEGNER BEREICH (Immersive) --- - // Wir nutzen Flexible, damit der Gegner Platz einnimmt, aber schrumpft, wenn nötig Flexible( - flex: 4, // Verhältnis zum unteren Teil + flex: 4, child: Stack( alignment: Alignment.center, children: [ - // Gegner Bild (Groß & Frei) Padding( - padding: const EdgeInsets.only(bottom: 40), // Platz für HP Bar + padding: const EdgeInsets.only(bottom: 40), child: Image.asset( _getEnemyAsset(currentExercise.exerciseId), fit: BoxFit.contain, - // Ein Glow-Effekt hinter dem Gegner für bessere Abhebung vom Hintergrund color: Colors.white.withOpacity(0.9), colorBlendMode: BlendMode.modulate, ), ), - - // Wave Badge (Oben Rechts, dezent) Positioned( top: 16, right: 16, @@ -626,15 +609,12 @@ class _BattleScreenState extends ConsumerState { ), ), ), - - // HP Bar (Direkt unter dem Gegner, Schwebend) Positioned( bottom: 10, left: 32, right: 32, child: Column( children: [ - // Kleines Herz Icon const Icon(Icons.favorite, color: AppTheme.errorColor, size: 24), const SizedBox(height: 4), @@ -648,15 +628,11 @@ class _BattleScreenState extends ConsumerState { ], ), ), - - // --- 2. KONTROLL BEREICH (Scrollable) --- - // Dieser Teil enthält die Trainings-Infos und den Counter Expanded( flex: 6, child: Container( decoration: BoxDecoration( - color: AppTheme.surfaceColor - .withOpacity(0.95), // Fast undurchsichtig für Lesbarkeit + color: AppTheme.surfaceColor.withOpacity(0.95), borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), boxShadow: [ @@ -670,8 +646,7 @@ class _BattleScreenState extends ConsumerState { children: [ Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB( - 24, 24, 24, 100), // Unten Platz für Button + padding: const EdgeInsets.fromLTRB(24, 24, 24, 100), child: Column( children: [ Text( @@ -692,10 +667,7 @@ class _BattleScreenState extends ConsumerState { .titleMedium ?.copyWith(color: Colors.white70), ), - const SizedBox(height: 24), - - // Target Info (Kompakt) Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -708,10 +680,7 @@ class _BattleScreenState extends ConsumerState { '${currentSet.repsTarget}${currentSet.isAmrap ? "+" : ""}'), ], ), - const SizedBox(height: 24), - - // Load / Assistance Visualizer if (plateResult.bandAssistance != null) Container( padding: const EdgeInsets.all(12), @@ -752,41 +721,7 @@ class _BattleScreenState extends ConsumerState { isTwoSided: currentExercise.exerciseId == 'squat', exerciseName: currentExercise.exerciseName, ), - const SizedBox(height: 32), - - // // Counter (Groß) - // Text('REPS COMPLETED', - // style: TextStyle( - // color: Colors.grey, - // fontSize: 12, - // letterSpacing: 1.5, - // fontWeight: FontWeight.bold)), - // const SizedBox(height: 8), - // Row( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // _CounterButton( - // icon: Icons.remove, - // onTap: _repsCompleted > 0 - // ? () => setState(() => _repsCompleted--) - // : null), - // Container( - // width: 100, - // alignment: Alignment.center, - // child: Text( - // '$_repsCompleted', - // style: const TextStyle( - // fontSize: 64, - // fontWeight: FontWeight.bold, - // color: Colors.white), - // ), - // ), - // _CounterButton( - // icon: Icons.add, - // onTap: () => setState(() => _repsCompleted++)), - // ], - // ), ], ), ), @@ -795,20 +730,14 @@ class _BattleScreenState extends ConsumerState { ), ), ), - - // --- 3. FIXIERTER BUTTON --- Container( - color: - AppTheme.surfaceColor, // Gleiche Farbe wie der Kontroll-Container + color: AppTheme.surfaceColor, padding: const EdgeInsets.all(16), child: SafeArea( top: false, child: SizedBox( width: double.infinity, child: ElevatedButton( - // onPressed: _repsCompleted >= currentSet.repsTarget - // ? _completeSet - // : null, onPressed: () => _handleCompletePress(currentSet), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), @@ -834,7 +763,6 @@ class _BattleScreenState extends ConsumerState { if (currentSet.isAmrap) { _showAmrapDialog(currentSet); } else { - // Standard-Satz: Wir gehen davon aus, dass das Ziel erreicht wurde setState(() { _repsCompleted = currentSet.repsTarget; }); @@ -849,7 +777,6 @@ class _BattleScreenState extends ConsumerState { } void _showAmrapDialog(WorkoutSet set) { - // Startwert ist das Ziel (oder was bisher eingestellt war) int tempReps = set.repsTarget; showModalBottomSheet( @@ -860,8 +787,7 @@ class _BattleScreenState extends ConsumerState { borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (context) { - return StatefulBuilder(// Wichtig für State im Dialog - builder: (context, setModalState) { + return StatefulBuilder(builder: (context, setModalState) { return Padding( padding: const EdgeInsets.all(24), child: Column( @@ -880,8 +806,6 @@ class _BattleScreenState extends ConsumerState { style: TextStyle(color: Colors.grey), ), const SizedBox(height: 32), - - // Großer Counter Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -906,16 +830,13 @@ class _BattleScreenState extends ConsumerState { onTap: () => setModalState(() => tempReps++)), ], ), - const SizedBox(height: 32), - SizedBox( width: double.infinity, height: 56, child: ElevatedButton( onPressed: () { - Navigator.pop(context); // Dialog schließen - // Wert übernehmen und Satz beenden + Navigator.pop(context); setState(() { _repsCompleted = tempReps; }); @@ -930,7 +851,7 @@ class _BattleScreenState extends ConsumerState { fontSize: 18, fontWeight: FontWeight.bold)), ), ), - const SizedBox(height: 16), // Puffer für iOS Home Bar + const SizedBox(height: 16), ], ), ); @@ -963,7 +884,6 @@ class _InfoBox extends StatelessWidget { } } -// Der Counter Button Helper (kannst du so lassen oder anpassen) class _CounterButton extends StatelessWidget { final IconData icon; final VoidCallback? onTap; 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 c8f3b22..08f213f 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 @@ -45,10 +45,8 @@ class EnemyHPBar extends StatelessWidget { ], ), const SizedBox(height: 8), - Stack( children: [ - // Background Container( height: 24, decoration: BoxDecoration( @@ -60,8 +58,6 @@ class EnemyHPBar extends StatelessWidget { ), ), ), - - // HP Fill FractionallySizedBox( widthFactor: percentage.clamp(0.0, 1.0), child: Container( @@ -89,4 +85,3 @@ class EnemyHPBar extends StatelessWidget { ); } } - 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 58a987a..28941c0 100644 --- a/lib/src/features/workout_runner/presentation/widgets/plate_visualizer.dart +++ b/lib/src/features/workout_runner/presentation/widgets/plate_visualizer.dart @@ -77,14 +77,11 @@ class PlateVisualizer extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Left collar Container( width: 8, height: 80, color: Colors.grey[800], ), - - // Plates (from largest to smallest) ...plateConfiguration.map((weight) { final size = _getPlateSize(weight); return Container( @@ -98,8 +95,6 @@ class PlateVisualizer extends StatelessWidget { ), ); }).toList(), - - // Sleeve (bar end) Container( width: 40, height: 20, @@ -155,7 +150,6 @@ class PlateVisualizer extends StatelessWidget { } double _getPlateSize(double weight) { - // Scale plate size based on weight if (weight >= 20) return 120.0; if (weight >= 10) return 100.0; if (weight >= 5) return 80.0; diff --git a/lib/src/shared/data/local/collections/user_collection.dart b/lib/src/shared/data/local/collections/user_collection.dart index e581e83..7dd3978 100644 --- a/lib/src/shared/data/local/collections/user_collection.dart +++ b/lib/src/shared/data/local/collections/user_collection.dart @@ -5,22 +5,21 @@ part 'user_collection.g.dart'; @collection class UserCollection { Id id = Isar.autoIncrement; - + @Index(unique: true) - String? serverId; // PocketBase ID - + String? serverId; + String email = ''; int xp = 0; int level = 1; double currentBodyweight = 70.0; - - String? inventorySettingsJson; // JSON string - String? avatarConfigJson; // JSON string - + + String? inventorySettingsJson; + String? avatarConfigJson; + DateTime? lastSyncAt; - bool isDirty = false; // Needs sync - + bool isDirty = false; + DateTime createdAt = DateTime.now(); DateTime updatedAt = DateTime.now(); } - diff --git a/lib/src/shared/data/remote/api_client.dart b/lib/src/shared/data/remote/api_client.dart index e36c64a..d8d63a0 100644 --- a/lib/src/shared/data/remote/api_client.dart +++ b/lib/src/shared/data/remote/api_client.dart @@ -27,7 +27,6 @@ class ApiClient { ), ); - // Add interceptors _dio.interceptors.add( PrettyDioLogger( requestHeader: true, @@ -39,7 +38,6 @@ class ApiClient { ), ); - // Auth token interceptor _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { @@ -60,7 +58,6 @@ class ApiClient { ); } - // Authentication Future> login(String email, String password) async { try { final response = await _dio.post( @@ -120,7 +117,6 @@ class ApiClient { await _storage.delete(key: AppConstants.keyUserId); } - // Sync Future> sync({ required String lastSyncTimestamp, required Map pushData, @@ -140,7 +136,6 @@ class ApiClient { } } - // Cycle Management Future> createCycle( Map trainingMaxes) async { try { @@ -178,7 +173,6 @@ class ApiClient { } } - // Stats Future> getStatsHistory({ required String exercise, required String range, @@ -208,7 +202,6 @@ class ApiClient { } } - // Profile Future updateBodyweight(double bodyweight) async { try { await _dio.patch( @@ -240,7 +233,6 @@ class ApiClient { required String newPasswordConfirm, }) async { try { - // PocketBase erwartet oldPassword, password, passwordConfirm await _dio.patch( '${ApiEndpoints.userUpdate}/$userId', data: { diff --git a/lib/src/shared/data/remote/sync_service.dart b/lib/src/shared/data/remote/sync_service.dart index 0786282..543df98 100644 --- a/lib/src/shared/data/remote/sync_service.dart +++ b/lib/src/shared/data/remote/sync_service.dart @@ -32,21 +32,15 @@ class SyncService { try { debugPrint('🔄 Starting Sync...'); - - // --------------------------------------------------------- - // STEP 1: Sync Cycles First (Parents of Workouts) - // --------------------------------------------------------- final dirtyCycles = await isar.cycleCollections.filter().isDirtyEqualTo(true).findAll(); for (var cycle in dirtyCycles) { try { if (cycle.serverId == null) { - // Create new cycle on server debugPrint( '📤 Pushing new cycle ${cycle.cycleNumber} to server...'); - // Parse TMs safely Map tmsMap = {}; try { final tms = jsonDecode(cycle.trainingMaxesJson); @@ -54,7 +48,6 @@ class SyncService { tms.map((k, v) => MapEntry(k, (v as num).toDouble()))); } catch (e) { debugPrint('⚠️ Error parsing TMs for cycle ${cycle.id}: $e'); - // Default fallback if parsing fails tmsMap = {'squat': 0.0, 'pullup': 0.0, 'dip': 0.0}; } @@ -62,13 +55,10 @@ class SyncService { final newServerId = response['id']; await isar.writeTxn(() async { - // Update cycle with server ID cycle.serverId = newServerId; cycle.isDirty = false; await isar.cycleCollections.put(cycle); - // CRITICAL: Update all workouts that linked to the local ID of this cycle - // Since we stored 'local ID string' in cycleId for offline workouts final oldLocalIdRef = cycle.id.toString(); final orphanWorkouts = await isar.workoutCollections @@ -77,15 +67,13 @@ class SyncService { .findAll(); for (var w in orphanWorkouts) { - w.cycleId = newServerId; // Update link to valid server ID - w.isDirty = true; // Ensure it gets picked up in next step + w.cycleId = newServerId; + w.isDirty = true; await isar.workoutCollections.put(w); debugPrint('🔗 Relinked workout ${w.id} to cycle $newServerId'); } }); } else { - // Cycle already has server ID but marked dirty -> Update on server if needed - // For MVP we assume cycles are immutable except for status, skipping update logic to avoid complexity await isar.writeTxn(() async { cycle.isDirty = false; await isar.cycleCollections.put(cycle); @@ -93,16 +81,10 @@ class SyncService { } } catch (e) { debugPrint('❌ Failed to sync cycle: $e'); - // We stop here because workouts depend on cycles. return; } } - // --------------------------------------------------------- - // STEP 2: Sync Workouts & User Stats - // --------------------------------------------------------- - - // 1. Gather local changes final dirtyUser = await isar.userCollections.filter().isDirtyEqualTo(true).findFirst(); @@ -112,18 +94,14 @@ class SyncService { if (dirtyUser == null && dirtyWorkouts.isEmpty) { debugPrint('✅ Nothing to push.'); } else { - // 2. Prepare Push Data final pushData = { 'workouts': dirtyWorkouts.where((w) { - // Filter out workouts that still don't have a valid cycle Server ID (e.g. if cycle sync failed) - // A valid PocketBase ID is 15 chars. A local ID is usually "1", "2". - // This is a heuristic check. return w.cycleId.length > 5; }).map((w) { return { 'id': w.serverId, 'local_id': w.id, - 'cycle_id': w.cycleId, // Must be a Server ID + 'cycle_id': w.cycleId, 'week': w.week, 'day': w.day, 'completed_at': w.completedAt?.toIso8601String(), @@ -141,16 +119,13 @@ class SyncService { : null, }; - // If we filtered out workouts, log it if ((pushData['workouts'] as List).length < dirtyWorkouts.length) { debugPrint( '⚠️ Skipped some workouts because they lack a valid server cycle ID.'); } - // 3. Get Last Sync Timestamp final lastSync = await _storage.read(key: AppConstants.keyLastSync); - // 4. Call API if ((pushData['workouts'] as List).isNotEmpty || pushData['user_stats'] != null) { debugPrint('📤 Pushing data...'); @@ -159,25 +134,17 @@ class SyncService { pushData: pushData, ); - // 5. Process Response await isar.writeTxn(() async { - // Update User if (dirtyUser != null) { dirtyUser.isDirty = false; await isar.userCollections.put(dirtyUser); } - // Update pushed workouts (Clear dirty flags) for (var w in dirtyWorkouts) { - // We assume success if no error thrown - // Ideally we match IDs from response, but for MVP optimistically clearing is okay - // providing we don't overwrite serverId if it was null. - // The server usually returns the new/updated records in 'pull_data' anyway. w.isDirty = false; await isar.workoutCollections.put(w); } - // Process Pulled Workouts (Updates from Server) if (response['pull_data'] != null && response['pull_data']['workouts'] != null) { final pulledWorkouts = response['pull_data']['workouts'] as List; @@ -207,7 +174,6 @@ class SyncService { } }); - // 6. Save new Sync Timestamp if (response['server_timestamp'] != null) { await _storage.write( key: AppConstants.keyLastSync, diff --git a/lib/src/shared/data/repositories/cycle_repository.dart b/lib/src/shared/data/repositories/cycle_repository.dart index e16b63f..959de45 100644 --- a/lib/src/shared/data/repositories/cycle_repository.dart +++ b/lib/src/shared/data/repositories/cycle_repository.dart @@ -88,14 +88,10 @@ class CycleRepository { final cycleIdRef = currentCycle.serverId ?? currentCycle.id.toString(); - // --- FIX START: Vollständigkeitsprüfung --- - // Wir zählen, wie viele Workouts in Woche 1, 2 und 3 tatsächlich abgeschlossen wurden. - // Es müssen genau 9 sein (3 Wochen * 3 Tage). final completedMainWorkouts = await isar.workoutCollections .filter() - .weekLessThan( - 4) // Nur Woche 1-3 zählen (Deload Woche 4 ist optional für Finish) - .completedAtIsNotNull() // Nur abgeschlossene zählen + .weekLessThan(4) + .completedAtIsNotNull() .group((q) => q .cycleIdEqualTo(cycleIdRef) .or() @@ -116,8 +112,6 @@ class CycleRepository { 'dip': (currentTMs['dip'] as num?)?.toDouble() ?? 0.0, }; - // final cycleIdRef = currentCycle.serverId ?? currentCycle.id.toString(); - final week3Workouts = await isar.workoutCollections .filter() .weekEqualTo(3) diff --git a/lib/src/shared/data/repositories/user_repository.dart b/lib/src/shared/data/repositories/user_repository.dart index 6ff570d..35e248b 100644 --- a/lib/src/shared/data/repositories/user_repository.dart +++ b/lib/src/shared/data/repositories/user_repository.dart @@ -191,60 +191,27 @@ class UserRepository { if (user?.serverId != null) { await apiClient.deleteAccount(user!.serverId!); } - // Lokal alles löschen await logout(); } - // Future resetProgress() async { - // final user = await getLocalUser(); - // if (user != null) { - // // 1. User Stats reset - // user.xp = 0; - // user.level = 1; - // user.isDirty = true; - - // await isar.writeTxn(() async { - // await isar.userCollections.put(user); - - // // 2. Alle Cycles und Workouts löschen - // await isar.cycleCollections.clear(); - // await isar.workoutCollections.clear(); - // }); - - // // Sync anstoßen, um Server zu aktualisieren (User Stats) - // // Hinweis: Das Löschen der History auf dem Server erfordert ggf. separate Logik, - // // da der Sync aktuell nur "Updates" pusht, aber keine "Deletes" für Listen. - // // Für MVP reicht der lokale Reset + User Stats Update. - // } - // } Future resetProgress() async { final user = await getLocalUser(); if (user != null) { - // 1. SERVER RESET (Zwingend zuerst!) try { - // Wir versuchen, den Server zu bereinigen. await apiClient.resetProgress(); } catch (e) { - // Wenn das fehlschlägt (z.B. Offline), brechen wir ab. - // Ein lokaler Reset ohne Server-Reset führt sonst zu Daten-Chaos beim nächsten Sync. throw Exception( "Server connection required to reset progress. Please try again when online."); } - // 2. LOKALER RESET (Nur wenn Server erfolgreich war) user.xp = 0; user.level = 1; - // Wichtig: Wir setzen isDirty auf FALSE. - // Der Server weiß schon Bescheid (durch den API Call oben). - // Wir müssen ihm nicht nochmal sagen, dass XP jetzt 0 ist. user.isDirty = false; await isar.writeTxn(() async { - // User speichern await isar.userCollections.put(user); - // Alle lokalen Trainingsdaten löschen await isar.cycleCollections.clear(); await isar.workoutCollections.clear(); }); diff --git a/lib/src/shared/data/repositories/workout_repository.dart b/lib/src/shared/data/repositories/workout_repository.dart index 2708001..fd38d1e 100644 --- a/lib/src/shared/data/repositories/workout_repository.dart +++ b/lib/src/shared/data/repositories/workout_repository.dart @@ -74,21 +74,9 @@ class WorkoutRepository { await saveWorkout(workout); } - // Future getWorkoutByWeekDay({ - // required String cycleId, - // required int week, - // required int day, - // }) async { - // return await isar.workoutCollections - // .filter() - // .cycleIdEqualTo(cycleId) - // .weekEqualTo(week) - // .dayEqualTo(day) - // .findFirst(); - // } Future getWorkoutByWeekDay({ - required String cycleId, // Meist Server ID - String? localCycleId, // NEU: Backup Local ID + required String cycleId, + String? localCycleId, required int week, required int day, }) async { @@ -97,7 +85,6 @@ class WorkoutRepository { .weekEqualTo(week) .dayEqualTo(day) .group((q) { - // Wir suchen ENTWEDER nach der Server-ID ODER nach der lokalen ID var query = q.cycleIdEqualTo(cycleId); if (localCycleId != null) { query = query.or().cycleIdEqualTo(localCycleId); diff --git a/lib/src/shared/domain/logic/plate_calculator.dart b/lib/src/shared/domain/logic/plate_calculator.dart index 5ddb1f4..8a665e5 100644 --- a/lib/src/shared/domain/logic/plate_calculator.dart +++ b/lib/src/shared/domain/logic/plate_calculator.dart @@ -1,169 +1,9 @@ -// import 'dart:math'; - -// class PlateLoadResult { -// final bool success; -// final List plateConfiguration; -// final String? bandAssistance; // Name of the band needed -// final double totalAchieved; -// final String message; - -// PlateLoadResult({ -// required this.success, -// required this.plateConfiguration, -// this.bandAssistance, -// required this.totalAchieved, -// this.message = '', -// }); -// } - -// class PlateCalculator { -// /// Calculate plate loading for a target weight -// /// -// /// [targetWeight]: Total weight to achieve -// /// [barWeight]: Weight of the bar (or Bodyweight for calisthenics) -// /// [availablePlates]: List of available plate weights -// /// [availableBands]: Map of Band Name -> Resistance in KG -// /// [isTwoSided]: true for barbell, false for dip belt -// static PlateLoadResult calculate({ -// required double targetWeight, -// required double barWeight, -// required List availablePlates, -// Map availableBands = const {}, -// required bool isTwoSided, -// }) { -// double needed = targetWeight - barWeight; - -// if (needed < 0 && !isTwoSided) { -// final deficit = needed.abs(); -// String? bestBand; -// double minDiff = double.infinity; - -// availableBands.forEach((name, resistance) { -// final diff = (resistance - deficit).abs(); -// if (diff < minDiff) { -// minDiff = diff; -// bestBand = name; -// } -// }); - -// if (bestBand != null) { -// return PlateLoadResult( -// success: true, -// plateConfiguration: [], -// bandAssistance: bestBand, -// totalAchieved: targetWeight, -// message: 'Use $bestBand band for assistance', -// ); -// } - -// return PlateLoadResult( -// success: false, -// plateConfiguration: [], -// totalAchieved: barWeight, -// message: 'Need assistance but no bands available', -// ); -// } - -// if (needed <= 0.1) { -// return PlateLoadResult( -// success: true, -// plateConfiguration: [], -// totalAchieved: barWeight, -// message: isTwoSided ? 'Bar only' : 'Bodyweight Only', -// ); -// } - -// final sortedPlates = List.from(availablePlates) -// ..sort((a, b) => b.compareTo(a)); - -// if (sortedPlates.isEmpty) { -// return PlateLoadResult( -// success: false, -// plateConfiguration: [], -// totalAchieved: barWeight, -// message: 'No plates available', -// ); -// } - -// // ROUNDING LOGIC: -// // We must round the needed weight to the nearest increment of the smallest plate. -// // Example: Needed 9.7kg, Smallest plate 1.25kg -> Round to 10.0kg (8 * 1.25). -// final smallestPlate = sortedPlates.last; -// final targetPerSideRaw = isTwoSided ? needed / 2 : needed; - -// final roundedPerSide = -// (targetPerSideRaw / smallestPlate).round() * smallestPlate; - -// if (roundedPerSide <= 0.001) { -// return PlateLoadResult( -// success: true, -// plateConfiguration: [], -// totalAchieved: barWeight, -// message: isTwoSided ? 'Bar only' : 'Bodyweight Only', -// ); -// } - -// final result = _greedyFit(roundedPerSide, sortedPlates); - -// if (!result.success) { -// return PlateLoadResult( -// success: false, -// plateConfiguration: [], -// totalAchieved: barWeight, -// message: 'Cannot achieve target with available plates', -// ); -// } - -// final totalLoaded = isTwoSided -// ? barWeight + (result.totalWeight * 2) -// : barWeight + result.totalWeight; - -// return PlateLoadResult( -// success: true, -// plateConfiguration: result.plates, -// totalAchieved: totalLoaded, -// ); -// } - -// static _FitResult _greedyFit(double target, List plates) { -// final loaded = []; -// var remaining = target; - -// const epsilon = 0.01; - -// for (final plate in plates) { -// while (remaining >= plate - epsilon) { -// loaded.add(plate); -// remaining -= plate; -// } -// } - -// final success = remaining.abs() < epsilon; -// return _FitResult( -// success: success, -// plates: loaded, -// totalWeight: loaded.fold(0.0, (sum, p) => sum + p), -// ); -// } -// } - -// class _FitResult { -// final bool success; -// final List plates; -// final double totalWeight; - -// _FitResult({ -// required this.success, -// required this.plates, -// required this.totalWeight, -// }); -// } import 'dart:math'; class PlateLoadResult { final bool success; final List plateConfiguration; - final String? bandAssistance; // Name of the band needed + final String? bandAssistance; final double totalAchieved; final String message; @@ -193,14 +33,12 @@ class PlateCalculator { }) { double needed = targetWeight - barWeight; - // 1. Handle Assistance (Negative weight needed) if (needed < 0 && !isTwoSided) { final deficit = needed.abs(); String? bestBand; double closestResistance = 0.0; double minDiff = double.infinity; - // Finde das am besten passende Band availableBands.forEach((name, resistance) { final diff = (resistance - deficit).abs(); if (diff < minDiff) { @@ -233,19 +71,15 @@ class PlateCalculator { ); } - // Band gefunden und es ist sinnvoll return PlateLoadResult( success: true, plateConfiguration: [], bandAssistance: bestBand, - // Wir geben hier das echte erreichte Gewicht an (Körpergewicht - Bandstärke) totalAchieved: barWeight - closestResistance, message: 'Use $bestBand band for assistance', ); } - // 2. Handle Added Weight (Plates) - // Check if we effectively need 0 weight (with small tolerance) if (needed <= 0.1) { return PlateLoadResult( success: true, @@ -255,7 +89,6 @@ class PlateCalculator { ); } - // Sort plates descending to find smallest plate later final sortedPlates = List.from(availablePlates) ..sort((a, b) => b.compareTo(a)); @@ -268,11 +101,9 @@ class PlateCalculator { ); } - // ROUNDING LOGIC (wie vorher besprochen) final smallestPlate = sortedPlates.last; final targetPerSideRaw = isTwoSided ? needed / 2 : needed; - // Round to nearest smallest plate final roundedPerSide = (targetPerSideRaw / smallestPlate).round() * smallestPlate; @@ -285,7 +116,6 @@ class PlateCalculator { ); } - // Try to fit the ROUNDED weight final result = _greedyFit(roundedPerSide, sortedPlates); if (!result.success) { @@ -308,7 +138,6 @@ class PlateCalculator { ); } - /// Greedy algorithm to fit plates static _FitResult _greedyFit(double target, List plates) { final loaded = []; var remaining = target; diff --git a/pubspec.lock b/pubspec.lock index c924bb9..a06b297 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -285,10 +285,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" + sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9" url: "https://pub.dev" source: hosted - version: "0.66.2" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -306,10 +306,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "6.0.0" flutter_riverpod: dependency: "direct main" description: @@ -420,10 +420,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104 url: "https://pub.dev" source: hosted - version: "14.8.1" + version: "17.0.0" google_fonts: dependency: "direct main" description: @@ -468,10 +468,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -556,10 +556,10 @@ packages: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "6.0.0" logger: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 629c051..4bc1368 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,19 +35,19 @@ dependencies: shimmer: ^3.0.0 # Utilities - intl: ^0.19.0 + intl: ^0.20.2 freezed_annotation: ^2.4.1 json_annotation: ^4.9.0 equatable: ^2.0.5 logger: ^2.3.0 - fl_chart: ^0.66.0 + fl_chart: ^1.1.1 - go_router: ^14.2.0 + go_router: ^17.0.0 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.0 + flutter_lints: ^6.0.0 # Code Generation build_runner: ^2.4.9