refactor: perform clean up

This commit is contained in:
Patryk Hegenberg 2025-11-28 16:41:18 +01:00
parent 7e4dd30599
commit d680030b16
30 changed files with 99 additions and 1756 deletions

View file

@ -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<double, int> 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'; import 'dart:ui';
class AssetPaths { class AssetPaths {
// Backgrounds
static const String bgSplash = 'assets/images/backgrounds/splash.png'; static const String bgSplash = 'assets/images/backgrounds/splash.png';
static const String bgStreetParkDay = static const String bgStreetParkDay =
'assets/images/backgrounds/street_park_day.png'; 'assets/images/backgrounds/street_park_day.png';
@ -60,25 +11,20 @@ class AssetPaths {
static const String bgCommercialGym = static const String bgCommercialGym =
'assets/images/backgrounds/commercial_gym.png'; 'assets/images/backgrounds/commercial_gym.png';
// Avatars - Bases
static const String avatarMaleBase = 'assets/images/avatars/base/male.png'; static const String avatarMaleBase = 'assets/images/avatars/base/male.png';
static const String avatarFemaleBase = static const String avatarFemaleBase =
'assets/images/avatars/base/female.png'; 'assets/images/avatars/base/female.png';
// Avatars - Hair (Beispiele)
static const String hairShort = 'assets/images/avatars/hair/short.png'; static const String hairShort = 'assets/images/avatars/hair/short.png';
static const String hairLong = 'assets/images/avatars/hair/long.png'; static const String hairLong = 'assets/images/avatars/hair/long.png';
static const String hairBald = static const String hairBald = 'assets/images/avatars/hair/bald.png';
'assets/images/avatars/hair/bald.png'; // Transparent/Empty
// Avatars - Clothing (Beispiele)
static const String outfitBasicTee = static const String outfitBasicTee =
'assets/images/avatars/clothing/basic_tee.png'; 'assets/images/avatars/clothing/basic_tee.png';
static const String outfitHoodie = static const String outfitHoodie =
'assets/images/avatars/clothing/hoodie.png'; 'assets/images/avatars/clothing/hoodie.png';
static const String outfitTank = 'assets/images/avatars/clothing/tank.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 plate25kg = 'assets/images/plates/plate_25kg.png';
static const String plate20kg = 'assets/images/plates/plate_20kg.png'; static const String plate20kg = 'assets/images/plates/plate_20kg.png';
static const String plate15kg = 'assets/images/plates/plate_15kg.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 plate2_5kg = 'assets/images/plates/plate_2_5kg.png';
static const String plate1_25kg = 'assets/images/plates/plate_1_25kg.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 enemyIronGolem = 'assets/images/enemies/iron_golem.png';
static const String enemyGravityDemon = static const String enemyGravityDemon =
'assets/images/enemies/gravity_demon.png'; 'assets/images/enemies/gravity_demon.png';

View file

@ -117,7 +117,6 @@ final routerProvider = Provider<GoRouter>((ref) {
); );
}); });
// Splash Screen to determine initial route
class SplashScreen extends ConsumerStatefulWidget { class SplashScreen extends ConsumerStatefulWidget {
const SplashScreen({super.key}); const SplashScreen({super.key});
@ -141,10 +140,8 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
final user = await userRepo.getLocalUser(); final user = await userRepo.getLocalUser();
if (user == null) { if (user == null) {
// No user, go to login
context.go('/login'); context.go('/login');
} else { } else {
// User exists, go to hub
context.go('/hub'); context.go('/hub');
} }
} }
@ -154,27 +151,21 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
// 1. Hintergrundbild
Positioned.fill( Positioned.fill(
child: Image.asset( child: Image.asset(
AssetPaths.bgSplash, // Nutzt den Splash AssetPaths.bgSplash,
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
// 2. Overlay (Dunkel), damit Text lesbar ist
Positioned.fill( Positioned.fill(
child: Container( child: Container(
color: Colors.black.withOpacity(0.5), color: Colors.black.withOpacity(0.5),
), ),
), ),
// 3. Inhalt
Center( Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Logo Container (mit leichtem Glow)
Container( Container(
width: 120, width: 120,
height: 120, height: 120,
@ -228,45 +219,4 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
), ),
); );
} }
// @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),
// ),
// ],
// ),
// ),
// );
// }
} }

View file

@ -82,7 +82,6 @@ class AppTheme {
), ),
), ),
cardTheme: CardThemeData( cardTheme: CardThemeData(
// CardTheme -> CardThemeData
color: surfaceColor, color: surfaceColor,
elevation: 4, elevation: 4,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(

View file

@ -86,7 +86,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Title
Text( Text(
'WELCOME BACK', 'WELCOME BACK',
style: Theme.of(context).textTheme.displayMedium, style: Theme.of(context).textTheme.displayMedium,
@ -100,7 +99,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
), ),
const SizedBox(height: 48), const SizedBox(height: 48),
// Email Field
TextFormField( TextFormField(
controller: _emailController, controller: _emailController,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
@ -120,7 +118,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Password Field
TextFormField( TextFormField(
controller: _passwordController, controller: _passwordController,
obscureText: _obscurePassword, obscureText: _obscurePassword,
@ -150,7 +147,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Login Button
ElevatedButton( ElevatedButton(
onPressed: _isLoading ? null : _handleLogin, onPressed: _isLoading ? null : _handleLogin,
child: _isLoading child: _isLoading
@ -166,7 +162,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Register Link
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -189,4 +184,3 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
); );
} }
} }

View file

@ -8,7 +8,7 @@ import '../../../../shared/data/local/collections/user_collection.dart';
import '../../../gamification/domain/entities/avatar_config.dart'; import '../../../gamification/domain/entities/avatar_config.dart';
import '../../../gamification/presentation/widgets/avatar_editor.dart'; import '../../../gamification/presentation/widgets/avatar_editor.dart';
import '../../../gamification/presentation/widgets/avatar_renderer.dart'; import '../../../gamification/presentation/widgets/avatar_renderer.dart';
import 'dart:convert'; // Für jsonDecode import 'dart:convert';
class ProfileScreen extends ConsumerStatefulWidget { class ProfileScreen extends ConsumerStatefulWidget {
const ProfileScreen({super.key}); const ProfileScreen({super.key});
@ -39,14 +39,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
}); });
} }
} }
// Future<void> _loadUser() async {
// final user = await ref.read(userRepositoryProvider).getLocalUser();
// if (user != null && mounted) {
// setState(() {
// _currentBodyweight = user.currentBodyweight;
// });
// }
// }
Future<void> _saveBodyweight() async { Future<void> _saveBodyweight() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
@ -177,7 +169,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, // Wichtig für Fullscreen-Feeling isScrollControlled: true,
useSafeArea: true, useSafeArea: true,
backgroundColor: AppTheme.backgroundColor, backgroundColor: AppTheme.backgroundColor,
builder: (context) => Scaffold( builder: (context) => Scaffold(
@ -190,10 +182,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () { 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); Navigator.pop(context, _tempAvatarConfig);
}, },
child: const Text('SAVE', child: const Text('SAVE',
@ -205,14 +193,12 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
), ),
body: AvatarEditor( body: AvatarEditor(
initialConfig: currentConfig, initialConfig: currentConfig,
onChanged: (conf) => _tempAvatarConfig = onChanged: (conf) => _tempAvatarConfig = conf,
conf, // _tempAvatarConfig muss in State definiert werden
), ),
), ),
).then((result) async { ).then((result) async {
if (result is AvatarConfig) { if (result is AvatarConfig) {
setState(() => _isLoading = true); setState(() => _isLoading = true);
// Speichern
_user!.avatarConfigJson = jsonEncode(result.toJson()); _user!.avatarConfigJson = jsonEncode(result.toJson());
_user!.isDirty = true; _user!.isDirty = true;
await ref.read(userRepositoryProvider).saveLocalUser(_user!); await ref.read(userRepositoryProvider).saveLocalUser(_user!);
@ -267,13 +253,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
child: IconButton( child: IconButton(
icon: const Icon(Icons.edit, size: 16), icon: const Icon(Icons.edit, size: 16),
onPressed: _showAvatarEditor, onPressed: _showAvatarEditor,
// onPressed: () {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text(
// 'Avatar customization coming soon!')),
// );
// },
), ),
), ),
), ),
@ -281,8 +260,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Bodyweight Section
Text('Physical Stats', Text('Physical Stats',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
@ -330,8 +307,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Account Actions
Text('Account Security', Text('Account Security',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
@ -345,8 +320,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
onTap: _showChangePasswordDialog, onTap: _showChangePasswordDialog,
), ),
const Divider(), const Divider(),
// Danger Zone
const SizedBox(height: 24), const SizedBox(height: 24),
Text('Danger Zone', Text('Danger Zone',
style: Theme.of(context) style: Theme.of(context)
@ -414,7 +387,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () async { onPressed: () async {
await userRepo.logout(); await userRepo.logout();

View file

@ -170,7 +170,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
context.go('/battle', extra: { context.go('/battle', extra: {
'week': targetWeek, 'week': targetWeek,
'day': targetDay, 'day': targetDay,
'workoutId': workout!.id, 'workoutId': workout.id,
}); });
} }
} catch (e) { } catch (e) {
@ -223,11 +223,10 @@ class _HubScreenState extends ConsumerState<HubScreen> {
children: [ children: [
Positioned.fill( Positioned.fill(
child: Image.asset( child: Image.asset(
AssetPaths.bgStreetParkDay, // Das düstere Gym AssetPaths.bgStreetParkDay,
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
// Dunkler Overlay, damit die UI-Elemente gut lesbar sind
Positioned.fill( Positioned.fill(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -235,26 +234,13 @@ class _HubScreenState extends ConsumerState<HubScreen> {
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: [
Colors.black.withOpacity(0.6), // Oben etwas heller Colors.black.withOpacity(0.6),
Colors.black.withOpacity( Colors.black.withOpacity(0.85),
0.85), // Unten fast schwarz für Buttons
], ],
), ),
), ),
), ),
), ),
// Container(
// decoration: BoxDecoration(
// gradient: LinearGradient(
// begin: Alignment.topCenter,
// end: Alignment.bottomCenter,
// colors: [
// const Color(0xFF1A237E),
// AppTheme.backgroundColor,
// ],
// ),
// ),
// ),
Column( Column(
children: [ children: [
Padding( Padding(
@ -289,23 +275,8 @@ class _HubScreenState extends ConsumerState<HubScreen> {
const Spacer(flex: 1), const Spacer(flex: 1),
AvatarRenderer( AvatarRenderer(
config: avatarConfig, 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), const SizedBox(height: 24),
LevelDisplay(level: user.level), LevelDisplay(level: user.level),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -395,7 +366,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
}, },
), ),
_NavButton( _NavButton(
icon: Icons.auto_stories, // Buch Icon icon: Icons.auto_stories,
label: 'Codex', label: 'Codex',
onTap: () => context.go('/codex'), onTap: () => context.go('/codex'),
), ),

View file

@ -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<String, dynamic> json) {
// return AvatarConfig(
// gender: json['gender'] ?? 'male',
// skinTone: json['skin_tone'] ?? 'medium',
// hairStyle: json['hair_style'] ?? 'short_01',
// clothing: json['clothing'] ?? 'basic_tee',
// );
// }
// Map<String, dynamic> 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'; import '../../../../core/constants/asset_paths.dart';
class AvatarConfig { class AvatarConfig {

View file

@ -22,10 +22,10 @@ class CodexScreen extends StatelessWidget {
_LoreCard( _LoreCard(
name: 'Iron Golem', name: 'Iron Golem',
title: 'The Weight of the Earth', title: 'The Weight of the Earth',
description: description:
'Forged from the tectonic plates of the Deep Earth, the Iron Golem exists only to crush the weak. ' '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 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.', 'It respects only one thing: The raw power of the LEGS that can stand up against its crushing weight.',
assetPath: AssetPaths.enemyIronGolem, assetPath: AssetPaths.enemyIronGolem,
exercise: 'Squat Nemesis', exercise: 'Squat Nemesis',
color: Colors.redAccent, color: Colors.redAccent,
@ -34,10 +34,10 @@ class CodexScreen extends StatelessWidget {
_LoreCard( _LoreCard(
name: 'Gravity Demon', name: 'Gravity Demon',
title: 'The Abyssal Pull', title: 'The Abyssal Pull',
description: description:
'A spirit of pure downward force that clings to the back of adventurers. ' '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' '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.', 'Only those with a back of steel and the will to pull themselves up can escape its grasp.',
assetPath: AssetPaths.enemyGravityDemon, assetPath: AssetPaths.enemyGravityDemon,
exercise: 'Pull-up Nemesis', exercise: 'Pull-up Nemesis',
color: Colors.purpleAccent, color: Colors.purpleAccent,
@ -46,10 +46,10 @@ class CodexScreen extends StatelessWidget {
_LoreCard( _LoreCard(
name: 'Pressure Phantom', name: 'Pressure Phantom',
title: 'The Invisible Crusher', title: 'The Invisible Crusher',
description: description:
'An ethereal entity that compresses the very air around you. ' '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' '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.', 'Defeat it by pushing through the pain with explosive dipping power.',
assetPath: AssetPaths.enemyPressurePhantom, assetPath: AssetPaths.enemyPressurePhantom,
exercise: 'Dip Nemesis', exercise: 'Dip Nemesis',
color: Colors.cyanAccent, color: Colors.cyanAccent,
@ -97,14 +97,13 @@ class _LoreCard extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Header Image
Container( Container(
height: 200, height: 200,
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black26, color: Colors.black26,
image: DecorationImage( image: DecorationImage(
image: AssetImage(AssetPaths.bgUndergroundGym), // Hintergrund für Atmosphäre image: AssetImage(AssetPaths.bgUndergroundGym),
fit: BoxFit.cover, fit: BoxFit.cover,
opacity: 0.3, opacity: 0.3,
), ),
@ -118,8 +117,6 @@ class _LoreCard extends StatelessWidget {
), ),
), ),
), ),
// Content
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
@ -130,14 +127,17 @@ class _LoreCard extends StatelessWidget {
children: [ children: [
Text( Text(
name.toUpperCase(), name.toUpperCase(),
style: Theme.of(context).textTheme.headlineSmall?.copyWith( style:
color: color, Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold, color: color,
letterSpacing: 1.5, fontWeight: FontWeight.bold,
), letterSpacing: 1.5,
),
), ),
Chip( 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, backgroundColor: Colors.black54,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
@ -155,9 +155,9 @@ class _LoreCard extends StatelessWidget {
Text( Text(
description, description,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
height: 1.5, height: 1.5,
color: Colors.white70, color: Colors.white70,
), ),
), ),
], ],
), ),

View file

@ -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<AvatarConfig> onChanged;
// const AvatarEditor({
// super.key,
// required this.initialConfig,
// required this.onChanged,
// });
// @override
// State<AvatarEditor> createState() => _AvatarEditorState();
// }
// class _AvatarEditorState extends State<AvatarEditor> {
// 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<String> 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 'package:flutter/material.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../domain/entities/avatar_config.dart'; import '../../domain/entities/avatar_config.dart';
@ -259,7 +40,6 @@ class _AvatarEditorState extends State<AvatarEditor> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// Preview
Container( Container(
height: 200, height: 200,
alignment: Alignment.center, alignment: Alignment.center,
@ -269,8 +49,6 @@ class _AvatarEditorState extends State<AvatarEditor> {
size: 160, size: 160,
), ),
), ),
// Gender Switch
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: SegmentedButton<String>( child: SegmentedButton<String>(
@ -280,8 +58,7 @@ class _AvatarEditorState extends State<AvatarEditor> {
], ],
selected: {_gender}, selected: {_gender},
onSelectionChanged: (Set<String> newSelection) { onSelectionChanged: (Set<String> newSelection) {
_update( _update(newSelection.first, 1);
newSelection.first, 1); // Reset to variant 1 on gender switch
}, },
style: ButtonStyle( style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith<Color>( backgroundColor: MaterialStateProperty.resolveWith<Color>(
@ -291,8 +68,6 @@ class _AvatarEditorState extends State<AvatarEditor> {
), ),
), ),
), ),
// Variants Grid
Expanded( Expanded(
child: GridView.builder( child: GridView.builder(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -301,7 +76,7 @@ class _AvatarEditorState extends State<AvatarEditor> {
crossAxisSpacing: 12, crossAxisSpacing: 12,
mainAxisSpacing: 12, mainAxisSpacing: 12,
), ),
itemCount: 8, // Wir haben 8 Varianten pro Sheet itemCount: 8,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final variantNum = index + 1; final variantNum = index + 1;
final isSelected = _variant == variantNum; final isSelected = _variant == variantNum;

View file

@ -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 'package:flutter/material.dart';
import '../../domain/entities/avatar_config.dart'; import '../../domain/entities/avatar_config.dart';

View file

@ -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<HistoryScreen> createState() => _HistoryScreenState();
// }
// class _HistoryScreenState extends ConsumerState<HistoryScreen> {
// @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<List<WorkoutCollection>>(
// 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 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -170,15 +36,13 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text( title: const Text('Quest Log'),
'Quest Log'), // "Quest Log" passt besser zum RPG Theme als "History"
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/hub'), onPressed: () => context.go('/hub'),
), ),
), ),
body: FutureBuilder<List<WorkoutCollection>>( body: FutureBuilder<List<WorkoutCollection>>(
// future: workoutRepo.getCompletedWorkouts(),
future: _loadHistory(), future: _loadHistory(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
@ -210,7 +74,6 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
); );
} }
// Sort by date descending (newest first)
final workouts = snapshot.data! final workouts = snapshot.data!
..sort((a, b) => b.completedAt!.compareTo(a.completedAt!)); ..sort((a, b) => b.completedAt!.compareTo(a.completedAt!));
@ -249,7 +112,6 @@ class _WorkoutHistoryCard extends StatelessWidget {
final timeStr = DateFormat.jm().format(workout.completedAt!); final timeStr = DateFormat.jm().format(workout.completedAt!);
final exercises = _parseExercises(); final exercises = _parseExercises();
// Zusammenfassung der trainierten Muskelgruppen/Übungen für den Titel
final summary = exercises final summary = exercises
.map((e) => .map((e) =>
e.exerciseName.replaceAll('Weighted ', '').replaceAll('Back ', '')) e.exerciseName.replaceAll('Weighted ', '').replaceAll('Back ', ''))

View file

@ -27,7 +27,6 @@ class PlateCounter extends StatelessWidget {
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Row( child: Row(
children: [ children: [
// Plate Visual
Container( Container(
width: 48, width: 48,
height: 48, height: 48,
@ -53,16 +52,12 @@ class PlateCounter extends StatelessWidget {
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
// Weight Label
Expanded( Expanded(
child: Text( child: Text(
'${weight.toStringAsFixed(weight == weight.toInt() ? 0 : 2)} kg', '${weight.toStringAsFixed(weight == weight.toInt() ? 0 : 2)} kg',
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
), ),
), ),
// Counter
IconButton( IconButton(
icon: const Icon(Icons.remove_circle_outline), icon: const Icon(Icons.remove_circle_outline),
onPressed: count > 0 ? () => onChanged(count - 1) : null, onPressed: count > 0 ? () => onChanged(count - 1) : null,
@ -73,7 +68,7 @@ class PlateCounter extends StatelessWidget {
height: 40, height: 40,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1), // Hintergrund color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: border:
Border.all(color: AppTheme.primaryColor.withOpacity(0.3)), Border.all(color: AppTheme.primaryColor.withOpacity(0.3)),
@ -81,25 +76,15 @@ class PlateCounter extends StatelessWidget {
child: Text( child: Text(
count.toString(), count.toString(),
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.white, // Explizit Weiß color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
// SizedBox(
// width: 32,
// child: Text(
// count.toString(),
// style: Theme.of(context).textTheme.titleLarge,
// textAlign: TextAlign.center,
// ),
// ),
IconButton( IconButton(
icon: const Icon(Icons.add_circle_outline), icon: const Icon(Icons.add_circle_outline),
onPressed: count < 20 onPressed: count < 20 ? () => onChanged(count + 1) : null,
? () => onChanged(count + 1)
: null, // Limit erhöht auf 20
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
), ),
], ],

View file

@ -9,7 +9,7 @@ import '../../../../shared/data/repositories/user_repository.dart';
import '../../../../shared/data/repositories/cycle_repository.dart'; import '../../../../shared/data/repositories/cycle_repository.dart';
import '../../../gamification/domain/entities/avatar_config.dart'; import '../../../gamification/domain/entities/avatar_config.dart';
import '../../../gamification/presentation/widgets/avatar_editor.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 { class AvatarSetupScreen extends ConsumerStatefulWidget {
const AvatarSetupScreen({super.key}); const AvatarSetupScreen({super.key});
@ -22,64 +22,6 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
AvatarConfig _config = const AvatarConfig(); AvatarConfig _config = const AvatarConfig();
bool _isLoading = false; bool _isLoading = false;
// Future<void> _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<String, dynamic>;
// // 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<String, dynamic>?;
// if (trainingMaxes != null) {
// final cycleRepo = ref.read(cycleRepositoryProvider);
// final tmMap = <String, double>{
// '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<void> _handleFinish() async { Future<void> _handleFinish() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
@ -89,11 +31,9 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
final inventorySettings = final inventorySettings =
onboardingData['inventory_settings'] as Map<String, dynamic>; onboardingData['inventory_settings'] as Map<String, dynamic>;
// PRÜFUNG: Sind wir schon eingeloggt? (Reset Fall)
var user = await userRepo.getLocalUser(); var user = await userRepo.getLocalUser();
if (user == null) { if (user == null) {
// FALL A: Neuer User -> Registrieren
user = await userRepo.register( user = await userRepo.register(
email: onboardingData['email'] ?? '', email: onboardingData['email'] ?? '',
password: onboardingData['password'] ?? '', password: onboardingData['password'] ?? '',
@ -101,14 +41,12 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
inventorySettings: inventorySettings, inventorySettings: inventorySettings,
); );
} else { } else {
// FALL B: Existierender User (Reset) -> Nur Updaten
user.currentBodyweight = user.currentBodyweight =
onboardingData['bodyweight'] ?? user.currentBodyweight; onboardingData['bodyweight'] ?? user.currentBodyweight;
user.inventorySettingsJson = jsonEncode(inventorySettings); user.inventorySettingsJson = jsonEncode(inventorySettings);
user.isDirty = true; user.isDirty = true;
await userRepo.saveLocalUser(user); await userRepo.saveLocalUser(user);
// Server Update triggern (via Repo Methoden die API rufen)
try { try {
await userRepo.updateBodyweight(user.currentBodyweight); await userRepo.updateBodyweight(user.currentBodyweight);
await userRepo.updateInventory(inventorySettings); await userRepo.updateInventory(inventorySettings);
@ -117,12 +55,10 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
} }
} }
// Avatar speichern (für beide Fälle gleich)
user!.avatarConfigJson = jsonEncode(_config.toJson()); user!.avatarConfigJson = jsonEncode(_config.toJson());
user.isDirty = true; user.isDirty = true;
await userRepo.saveLocalUser(user); await userRepo.saveLocalUser(user);
// Cycle erstellen (für beide Fälle gleich)
final trainingMaxes = final trainingMaxes =
onboardingData['training_maxes'] as Map<String, dynamic>?; onboardingData['training_maxes'] as Map<String, dynamic>?;
if (trainingMaxes != null) { if (trainingMaxes != null) {
@ -156,8 +92,7 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
// title: const Text('Create Character'), title: const Text('Choose Your Hero'),
title: const Text('Choose Your Hero'), // Statt "Create Character"
actions: [ actions: [
TextButton( TextButton(
onPressed: _isLoading ? null : _handleFinish, onPressed: _isLoading ? null : _handleFinish,
@ -193,10 +128,6 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
), ),
], ],
), ),
// body: AvatarEditor(
// initialConfig: _config,
// onChanged: (newConfig) => _config = newConfig,
// ),
); );
} }
} }

View file

@ -30,7 +30,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
1.25: 2, 1.25: 2,
}; };
// Band selection state - Default configuration based on standard colors
final Map<String, bool> _bandInventory = { final Map<String, bool> _bandInventory = {
'Blue': true, 'Blue': true,
'Green': true, 'Green': true,
@ -81,7 +80,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
} }
void _handleNext() { void _handleNext() {
// Listen bauen (wie vorher)
final platesList = <double>[]; final platesList = <double>[];
_plateInventory.forEach((weight, count) { _plateInventory.forEach((weight, count) {
for (int i = 0; i < count; i++) platesList.add(weight); for (int i = 0; i < count; i++) platesList.add(weight);
@ -104,13 +102,12 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
'bands': bandsList, 'bands': bandsList,
}; };
// Im Provider speichern für den nächsten Screen
ref.read(onboardingDataProvider.notifier).update((state) => { ref.read(onboardingDataProvider.notifier).update((state) => {
...state, ...state,
'inventory_settings': inventorySettings, 'inventory_settings': inventorySettings,
}); });
context.push('/onboarding/avatar'); // Neue Route! context.push('/onboarding/avatar');
} }
Future<void> _handleFinish() async { Future<void> _handleFinish() async {
@ -120,7 +117,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
final onboardingData = ref.read(onboardingDataProvider); final onboardingData = ref.read(onboardingDataProvider);
final userRepo = ref.read(userRepositoryProvider); final userRepo = ref.read(userRepositoryProvider);
// Build plates list for DB
final platesList = <double>[]; final platesList = <double>[];
_plateInventory.forEach((weight, count) { _plateInventory.forEach((weight, count) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@ -128,7 +124,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
} }
}); });
// Build bands list for DB
final bandsList = <Map<String, dynamic>>[]; final bandsList = <Map<String, dynamic>>[];
_bandInventory.forEach((color, isSelected) { _bandInventory.forEach((color, isSelected) {
if (isSelected) { if (isSelected) {
@ -146,7 +141,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
'bands': bandsList, 'bands': bandsList,
}; };
// Register user with all data
final user = await userRepo.register( final user = await userRepo.register(
email: onboardingData['email'] ?? '', email: onboardingData['email'] ?? '',
password: onboardingData['password'] ?? '', password: onboardingData['password'] ?? '',
@ -156,7 +150,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
debugPrint('✅ User registered: ${user.serverId}'); debugPrint('✅ User registered: ${user.serverId}');
// Create first cycle
final trainingMaxes = final trainingMaxes =
onboardingData['training_maxes'] as Map<String, dynamic>?; onboardingData['training_maxes'] as Map<String, dynamic>?;
@ -174,10 +167,8 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
} }
if (mounted) { if (mounted) {
// Clear onboarding data
ref.read(onboardingDataProvider.notifier).state = {}; ref.read(onboardingDataProvider.notifier).state = {};
// Navigate to hub
context.go('/hub'); context.go('/hub');
} }
} catch (e, stackTrace) { } catch (e, stackTrace) {
@ -186,7 +177,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
if (mounted) { if (mounted) {
String message = 'Setup failed: ${e.toString()}'; String message = 'Setup failed: ${e.toString()}';
// Catch unique constraint error (PocketBase returns 400 usually)
if (e.toString().toLowerCase().contains('unique') || if (e.toString().toLowerCase().contains('unique') ||
e.toString().toLowerCase().contains('email')) { e.toString().toLowerCase().contains('email')) {
message = 'Email already exists. Please login or use another email.'; message = 'Email already exists. Please login or use another email.';
@ -238,15 +228,12 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Progress Indicator
LinearProgressIndicator( LinearProgressIndicator(
value: 0.75, value: 0.75,
backgroundColor: AppTheme.xpBarBackground, backgroundColor: AppTheme.xpBarBackground,
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Title
Text( Text(
'Equipment Inventory', 'Equipment Inventory',
style: Theme.of(context).textTheme.displayMedium, style: Theme.of(context).textTheme.displayMedium,
@ -257,8 +244,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Bar Weight Selector
Text( Text(
'Barbell Weight', 'Barbell Weight',
style: Theme.of(context) style: Theme.of(context)
@ -286,8 +271,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Presets
Text( Text(
'Quick Presets', 'Quick Presets',
style: Theme.of(context) style: Theme.of(context)
@ -321,8 +304,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
], ],
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Plate Selection
Text( Text(
'Available Plates', 'Available Plates',
style: Theme.of(context) style: Theme.of(context)
@ -342,9 +323,7 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
}, },
); );
}).toList(), }).toList(),
const SizedBox(height: 32), const SizedBox(height: 32),
Text( Text(
'Resistance Bands (Assistance)', 'Resistance Bands (Assistance)',
style: Theme.of(context) style: Theme.of(context)
@ -385,27 +364,11 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
); );
}).toList(), }).toList(),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
ElevatedButton( ElevatedButton(
onPressed: _handleNext, onPressed: _handleNext,
child: const Text('NEXT STEP'), 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<InventorySetupScreen> {
); );
} }
} }
// class _PlateCounter extends StatelessWidget {
// final double weight;
// final int count;
// final ValueChanged<int> 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,
// ),
// ],
// ),
// ),
// );
// }
// }

View file

@ -17,7 +17,6 @@ class StrengthTestScreen extends ConsumerStatefulWidget {
class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> { class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
// Controllers for each exercise
final _squatWeightController = TextEditingController(text: '100'); final _squatWeightController = TextEditingController(text: '100');
final _squatRepsController = TextEditingController(text: '5'); final _squatRepsController = TextEditingController(text: '5');
final _pullupWeightController = TextEditingController(text: '0'); final _pullupWeightController = TextEditingController(text: '0');
@ -48,20 +47,17 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
void _calculateAll() { void _calculateAll() {
final bodyweight = ref.read(onboardingDataProvider)['bodyweight'] ?? 80.0; final bodyweight = ref.read(onboardingDataProvider)['bodyweight'] ?? 80.0;
// Squat
final squatWeight = double.tryParse(_squatWeightController.text) ?? 0; final squatWeight = double.tryParse(_squatWeightController.text) ?? 0;
final squatReps = int.tryParse(_squatRepsController.text) ?? 1; final squatReps = int.tryParse(_squatRepsController.text) ?? 1;
final squat1RM = WendlerCalculator.calculate1RM(squatWeight, squatReps); final squat1RM = WendlerCalculator.calculate1RM(squatWeight, squatReps);
final squatTM = WendlerCalculator.calculateTrainingMax(squat1RM); final squatTM = WendlerCalculator.calculateTrainingMax(squat1RM);
// Pullup (bodyweight + additional weight)
final pullupAdditional = double.tryParse(_pullupWeightController.text) ?? 0; final pullupAdditional = double.tryParse(_pullupWeightController.text) ?? 0;
final pullupReps = int.tryParse(_pullupRepsController.text) ?? 1; final pullupReps = int.tryParse(_pullupRepsController.text) ?? 1;
final pullupTotal = bodyweight + pullupAdditional; final pullupTotal = bodyweight + pullupAdditional;
final pullup1RM = WendlerCalculator.calculate1RM(pullupTotal, pullupReps); final pullup1RM = WendlerCalculator.calculate1RM(pullupTotal, pullupReps);
final pullupTM = WendlerCalculator.calculateTrainingMax(pullup1RM); final pullupTM = WendlerCalculator.calculateTrainingMax(pullup1RM);
// Dip (bodyweight + additional weight)
final dipAdditional = double.tryParse(_dipWeightController.text) ?? 0; final dipAdditional = double.tryParse(_dipWeightController.text) ?? 0;
final dipReps = int.tryParse(_dipRepsController.text) ?? 1; final dipReps = int.tryParse(_dipRepsController.text) ?? 1;
final dipTotal = bodyweight + dipAdditional; final dipTotal = bodyweight + dipAdditional;
@ -85,7 +81,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
void _handleContinue() { void _handleContinue() {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
// Store training maxes
ref.read(onboardingDataProvider.notifier).update((state) => { ref.read(onboardingDataProvider.notifier).update((state) => {
...state, ...state,
'training_maxes': _calculatedTMs, 'training_maxes': _calculatedTMs,
@ -114,17 +109,14 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Progress Indicator
LinearProgressIndicator( LinearProgressIndicator(
value: 0.5, value: 0.5,
backgroundColor: AppTheme.xpBarBackground, backgroundColor: AppTheme.xpBarBackground,
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Title
Text( Text(
'Combat Calibration', // Statt "Strength Assessment" 'Combat Calibration',
style: Theme.of(context).textTheme.displayMedium, style: Theme.of(context).textTheme.displayMedium,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -132,18 +124,12 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
'We need to assess your current power level to assign the correct monsters.', // Flavor 'We need to assess your current power level to assign the correct monsters.', // Flavor
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
// Text(
// 'Strength Assessment',
// style: Theme.of(context).textTheme.displayMedium,
// ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Enter your recent best performance for each exercise', 'Enter your recent best performance for each exercise',
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Squat
_ExerciseCard( _ExerciseCard(
exerciseName: 'Back Squat', exerciseName: 'Back Squat',
icon: Icons.accessibility_new, icon: Icons.accessibility_new,
@ -155,8 +141,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
onChanged: _calculateAll, onChanged: _calculateAll,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Pullup
_ExerciseCard( _ExerciseCard(
exerciseName: 'Weighted Pull-up', exerciseName: 'Weighted Pull-up',
icon: Icons.north, icon: Icons.north,
@ -169,8 +153,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
onChanged: _calculateAll, onChanged: _calculateAll,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Dip
_ExerciseCard( _ExerciseCard(
exerciseName: 'Weighted Dip', exerciseName: 'Weighted Dip',
icon: Icons.south, icon: Icons.south,
@ -183,8 +165,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
onChanged: _calculateAll, onChanged: _calculateAll,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Info Box
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -202,10 +182,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( 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( 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 '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) style: Theme.of(context)
@ -218,8 +194,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Continue Button
ElevatedButton( ElevatedButton(
onPressed: _handleContinue, onPressed: _handleContinue,
child: const Text('CONTINUE'), child: const Text('CONTINUE'),
@ -285,7 +259,6 @@ class _ExerciseCard extends StatelessWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Input Fields
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -341,7 +314,6 @@ class _ExerciseCard extends StatelessWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Calculations
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(

View file

@ -7,115 +7,22 @@ import '../../../../core/constants/asset_paths.dart';
class WelcomeScreen extends StatelessWidget { class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
// 1. Hintergrund (Street Park)
Positioned.fill( Positioned.fill(
child: Image.asset( child: Image.asset(
AssetPaths.bgStreetParkDay, AssetPaths.bgStreetParkDay,
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
// 2. Overlay (Dunkel für Lesbarkeit)
Positioned.fill( Positioned.fill(
child: Container( child: Container(
color: Colors.black.withOpacity(0.7), color: Colors.black.withOpacity(0.7),
), ),
), ),
// 3. Inhalt
SafeArea( SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
@ -124,8 +31,6 @@ class WelcomeScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Spacer(), const Spacer(),
// Logo (Optional: Kann bleiben oder weg)
Container( Container(
width: 100, width: 100,
height: 100, height: 100,
@ -142,10 +47,8 @@ class WelcomeScreen extends StatelessWidget {
size: 56, color: Colors.black), size: 56, color: Colors.black),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// RPG Title
Text( Text(
'ENTER THE ARENA', // Statt "WELCOME TO" 'ENTER THE ARENA',
style: Theme.of(context).textTheme.headlineMedium?.copyWith( style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Colors.white70, color: Colors.white70,
letterSpacing: 2, letterSpacing: 2,
@ -164,8 +67,6 @@ class WelcomeScreen extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// RPG Description
const Text( const Text(
'The Iron Golems have awakened. The Gravity Demons are pulling the world into the abyss.\n\n' '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?', '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, textAlign: TextAlign.center,
), ),
const SizedBox(height: 48), const SizedBox(height: 48),
// Features (Umformuliert)
_FeatureItem( _FeatureItem(
icon: Icons.shield, // Statt trending_up icon: Icons.shield,
title: 'Build Your Armor', title: 'Build Your Armor',
description: 'Progressive overload based on Wendler 5/3/1.', description: 'Progressive overload based on Wendler 5/3/1.',
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_FeatureItem( _FeatureItem(
icon: Icons.videogame_asset, icon: Icons.videogame_asset,
// icon: Icons
// .swords, // Statt videogame_asset (wenn Icon verfügbar, sonst Flash/Star)
title: 'Slay Monsters', title: 'Slay Monsters',
description: description:
'Turn every rep into damage against epic foes.', 'Turn every rep into damage against epic foes.',
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_FeatureItem( _FeatureItem(
icon: Icons.inventory_2, // Statt offline_bolt icon: Icons.inventory_2,
title: 'Gather Loot', title: 'Gather Loot',
description: 'Earn XP, level up, and unlock new gear.', description: 'Earn XP, level up, and unlock new gear.',
), ),
const Spacer(), const Spacer(),
// Button
ElevatedButton( ElevatedButton(
onPressed: () => context.go('/onboarding/bodyweight'), onPressed: () => context.go('/onboarding/bodyweight'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -211,7 +105,6 @@ class WelcomeScreen extends StatelessWidget {
fontWeight: FontWeight.bold, letterSpacing: 1)), fontWeight: FontWeight.bold, letterSpacing: 1)),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextButton( TextButton(
onPressed: () => context.go('/login'), onPressed: () => context.go('/login'),
child: const Text('Already a hero? Login here', child: const Text('Already a hero? Login here',

View file

@ -26,7 +26,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
String _selectedExercise = 'squat'; // squat, pullup, dip String _selectedExercise = 'squat'; // squat, pullup, dip
String _selectedRange = '3m'; // 1m, 3m, 1y, all String _selectedRange = '3m'; // 1m, 3m, 1y, all
// Daten
List<StatsDataPoint> _chartData = []; List<StatsDataPoint> _chartData = [];
bool _isChartLoading = true; bool _isChartLoading = true;
@ -49,7 +48,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
return; return;
} }
final userId = user.serverId ?? user.id.toString(); final userId = user.serverId ?? user.id.toString();
// 1. Alle abgeschlossenen Workouts laden (Lokal aus Isar)
final allWorkouts = await workoutRepo.getCompletedWorkouts(userId); final allWorkouts = await workoutRepo.getCompletedWorkouts(userId);
final points = <StatsDataPoint>[]; final points = <StatsDataPoint>[];
@ -57,7 +55,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
for (var workout in allWorkouts) { for (var workout in allWorkouts) {
if (workout.completedAt == null) continue; if (workout.completedAt == null) continue;
// 2. Exercises parsen
List<dynamic> exercisesJson = []; List<dynamic> exercisesJson = [];
try { try {
exercisesJson = jsonDecode(workout.exercisesJson); exercisesJson = jsonDecode(workout.exercisesJson);
@ -68,35 +65,26 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
double max1RM = 0.0; double max1RM = 0.0;
double sessionVolume = 0.0; double sessionVolume = 0.0;
bool foundExercise = false; bool foundExercise = false;
double trainingMax = double trainingMax = 0.0;
0.0; // Versuchen wir aus den Daten zu raten oder nehmen 0
// 3. Durch Übungen iterieren
for (var exJson in exercisesJson) { for (var exJson in exercisesJson) {
final exercise = Exercise.fromJson(exJson); final exercise = Exercise.fromJson(exJson);
// Nur die ausgewählte Übung betrachten
if (exercise.exerciseId == _selectedExercise) { if (exercise.exerciseId == _selectedExercise) {
foundExercise = true; foundExercise = true;
for (var set in exercise.sets) { for (var set in exercise.sets) {
if (!set.completed || set.repsActual <= 0) continue; if (!set.completed || set.repsActual <= 0) continue;
// Volumen summieren
sessionVolume += set.targetWeightTotal * set.repsActual; sessionVolume += set.targetWeightTotal * set.repsActual;
// 1RM berechnen (Epley Formel)
// Wir nutzen den WendlerCalculator, der die Logik schon hat
final e1rm = WendlerCalculator.calculate1RM( final e1rm = WendlerCalculator.calculate1RM(
set.targetWeightTotal, set.repsActual); set.targetWeightTotal, set.repsActual);
// Wir nehmen das beste Set des Tages als Wert für den Graphen
if (e1rm > max1RM) { if (e1rm > max1RM) {
max1RM = e1rm; max1RM = e1rm;
} }
// Versuchen, das TM aus dem Prozentsatz rückzurechnen (optional)
// TM = Weight / Percentage.
if (set.targetPercentage > 0 && trainingMax == 0) { if (set.targetPercentage > 0 && trainingMax == 0) {
trainingMax = trainingMax =
set.targetWeightTotal / (set.targetPercentage / 100.0); set.targetWeightTotal / (set.targetPercentage / 100.0);
@ -105,22 +93,18 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
} }
} }
// 4. Datenpunkt erstellen, wenn Übung in diesem Workout vorkam
if (foundExercise && max1RM > 0) { if (foundExercise && max1RM > 0) {
points.add(StatsDataPoint( points.add(StatsDataPoint(
date: workout.completedAt!, date: workout.completedAt!,
trainingMax: trainingMax: trainingMax,
trainingMax, // Ist ggf. ungenau durch Rückrechnung, aber für Graph ok
estimated1RM: max1RM, estimated1RM: max1RM,
totalVolume: sessionVolume, totalVolume: sessionVolume,
)); ));
} }
} }
// 5. Sortieren & Filtern (Zeitraum)
points.sort((a, b) => a.date.compareTo(b.date)); points.sort((a, b) => a.date.compareTo(b.date));
// Filter nach Datum (Range)
final now = DateTime.now(); final now = DateTime.now();
final filteredPoints = points.where((p) { final filteredPoints = points.where((p) {
if (_selectedRange == '1m') { if (_selectedRange == '1m') {
@ -130,7 +114,7 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
} else if (_selectedRange == '1y') { } else if (_selectedRange == '1y') {
return p.date.isAfter(now.subtract(const Duration(days: 365))); return p.date.isAfter(now.subtract(const Duration(days: 365)));
} }
return true; // 'all' return true;
}).toList(); }).toList();
if (mounted) { if (mounted) {
@ -149,45 +133,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
} }
} }
} }
// Future<void> _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) { void _onFilterChanged(String exercise, String range) {
setState(() { setState(() {
@ -202,17 +147,14 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
try { try {
final cycleRepo = ref.read(cycleRepositoryProvider); final cycleRepo = ref.read(cycleRepositoryProvider);
// 1. Alte TMs merken für den Vergleich
final oldTMs = final oldTMs =
jsonDecode(currentCycle.trainingMaxesJson) as Map<String, dynamic>; jsonDecode(currentCycle.trainingMaxesJson) as Map<String, dynamic>;
// 2. Zyklus abschließen (Stall Handling Logic läuft hier)
final newCycle = await cycleRepo.finishCycle(); final newCycle = await cycleRepo.finishCycle();
final newTMs = final newTMs =
jsonDecode(newCycle.trainingMaxesJson) as Map<String, dynamic>; jsonDecode(newCycle.trainingMaxesJson) as Map<String, dynamic>;
if (mounted) { if (mounted) {
// 3. Ergebnis anzeigen
await showDialog( await showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@ -223,7 +165,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
), ),
); );
// UI aktualisieren
setState(() {}); setState(() {});
} }
} catch (e) { } catch (e) {
@ -265,7 +206,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Cycle Card (bleibt wie vorher)
if (currentCycle != null) ...[ if (currentCycle != null) ...[
_CurrentCycleCard( _CurrentCycleCard(
cycle: currentCycle, cycle: currentCycle,
@ -285,7 +225,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// --- FILTER CHIPS ---
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
@ -312,7 +251,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// --- CHART ---
_isChartLoading _isChartLoading
? const SizedBox( ? const SizedBox(
height: 250, height: 250,
@ -322,58 +260,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
// (Optional: Range Selector unten drunter '1M', '3M', '1Y'...) // (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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
// title: Text('Cycle $newCycleNumber Started!'),
title: const Text('Dungeon Cleared!'), title: const Text('Dungeon Cleared!'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -501,8 +386,6 @@ class _CycleFinishDialog extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
const Text('Your Training Maxes have increased:', const Text('Your Training Maxes have increased:',
style: TextStyle(fontWeight: FontWeight.bold)), style: TextStyle(fontWeight: FontWeight.bold)),
// const Text(
// 'Based on your performance in Week 3, your Training Maxes have been updated:'),
const SizedBox(height: 16), const SizedBox(height: 16),
_DiffRow( _DiffRow(
name: 'Squat', oldVal: oldTMs['squat'], newVal: newTMs['squat']), name: 'Squat', oldVal: oldTMs['squat'], newVal: newTMs['squat']),
@ -517,7 +400,6 @@ class _CycleFinishDialog extends StatelessWidget {
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: const Text('ENTER NEXT LEVEL'), child: const Text('ENTER NEXT LEVEL'),
// child: const Text('LET\'S GO!'),
), ),
], ],
); );

View file

@ -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<StatsDataPoint> 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<StatsDataPoint>.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:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -178,13 +7,11 @@ import '../../domain/entities/stats_data_point.dart';
class ProgressChart extends StatelessWidget { class ProgressChart extends StatelessWidget {
final List<StatsDataPoint> data; final List<StatsDataPoint> data;
// FIX 1: 'const' Konstruktor erlaubt, da wir keine Berechnung mehr hier machen
const ProgressChart({ const ProgressChart({
super.key, super.key,
required this.data, required this.data,
}); });
// FIX 1: isEmpty als Getter (wird bei Zugriff berechnet)
bool get isEmpty => data.isEmpty; bool get isEmpty => data.isEmpty;
@override @override
@ -200,7 +27,6 @@ class ProgressChart extends StatelessWidget {
child: Text( child: Text(
'No data available yet', 'No data available yet',
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
// Kleine Anpassung für Typ-Sicherheit
color: AppTheme.textSecondary, color: AppTheme.textSecondary,
), ),
), ),
@ -208,17 +34,14 @@ class ProgressChart extends StatelessWidget {
); );
} }
// Daten sortieren
final points = List<StatsDataPoint>.from(data) final points = List<StatsDataPoint>.from(data)
..sort((a, b) => a.date.compareTo(b.date)); ..sort((a, b) => a.date.compareTo(b.date));
// Min/Max für Y-Achse berechnen (mit etwas Puffer)
double maxY = double maxY =
points.map((e) => e.estimated1RM).reduce((a, b) => a > b ? a : b); points.map((e) => e.estimated1RM).reduce((a, b) => a > b ? a : b);
double minY = double minY =
points.map((e) => e.estimated1RM).reduce((a, b) => a < b ? a : b); 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; maxY += 5;
minY = (minY - 5).clamp(0, double.infinity); minY = (minY - 5).clamp(0, double.infinity);
@ -263,9 +86,7 @@ class ProgressChart extends StatelessWidget {
sideTitles: SideTitles( sideTitles: SideTitles(
showTitles: true, showTitles: true,
reservedSize: 30, reservedSize: 30,
interval: (points.length / 3) interval: (points.length / 3).ceil().toDouble(),
.ceil()
.toDouble(), // Zeige nicht jedes Datum
getTitlesWidget: (value, meta) { getTitlesWidget: (value, meta) {
final index = value.toInt(); final index = value.toInt();
if (index >= 0 && index < points.length) { if (index >= 0 && index < points.length) {
@ -288,7 +109,7 @@ class ProgressChart extends StatelessWidget {
sideTitles: SideTitles( sideTitles: SideTitles(
showTitles: true, showTitles: true,
reservedSize: 40, reservedSize: 40,
interval: 5, // Alle 5kg eine Beschriftung interval: 5,
getTitlesWidget: (value, meta) { getTitlesWidget: (value, meta) {
return Text( return Text(
value.toInt().toString(), value.toInt().toString(),
@ -311,7 +132,7 @@ class ProgressChart extends StatelessWidget {
spots: points.asMap().entries.map((e) { spots: points.asMap().entries.map((e) {
return FlSpot(e.key.toDouble(), e.value.estimated1RM); return FlSpot(e.key.toDouble(), e.value.estimated1RM);
}).toList(), }).toList(),
isCurved: true, // Kurve glätten isCurved: true,
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
barWidth: 3, barWidth: 3,
isStrokeCapRound: true, isStrokeCapRound: true,
@ -334,7 +155,8 @@ class ProgressChart extends StatelessWidget {
lineTouchData: LineTouchData( lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData( touchTooltipData: LineTouchTooltipData(
// FIX 2: Alte API nutzen (tooltipBgColor statt getTooltipColor) // FIX 2: Alte API nutzen (tooltipBgColor statt getTooltipColor)
tooltipBgColor: AppTheme.surfaceColor, // tooltipBgColor: AppTheme.surfaceColor,
getTooltipColor: (touchedSpot) => AppTheme.surfaceColor,
getTooltipItems: (touchedSpots) { getTooltipItems: (touchedSpots) {
return touchedSpots.map((spot) { return touchedSpots.map((spot) {
final date = points[spot.x.toInt()].date; final date = points[spot.x.toInt()].date;

View file

@ -58,7 +58,6 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
} }
String _getEnemyAsset(String exerciseId) { String _getEnemyAsset(String exerciseId) {
// Mapping basierend auf Übungs-ID
switch (exerciseId) { switch (exerciseId) {
case 'squat': case 'squat':
return AssetPaths.enemyIronGolem; return AssetPaths.enemyIronGolem;
@ -67,7 +66,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
case 'dip': case 'dip':
return AssetPaths.enemyPressurePhantom; return AssetPaths.enemyPressurePhantom;
default: default:
return AssetPaths.enemyIronGolem; // Fallback return AssetPaths.enemyIronGolem;
} }
} }
@ -472,34 +471,25 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
), ),
body: Stack( body: Stack(
children: [ children: [
// 1. HINTERGRUND (Underground Gym)
Positioned.fill( Positioned.fill(
child: Image.asset( child: Image.asset(
AssetPaths.bgUndergroundGym, AssetPaths.bgUndergroundGym,
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
// 2. Overlay (Atmosphäre & Lesbarkeit)
Positioned.fill( Positioned.fill(
child: Container( child: Container(
color: Colors.black.withOpacity(0.7), // Dunkler Schleier color: Colors.black.withOpacity(0.7),
), ),
), ),
// 3. INHALT
SafeArea( SafeArea(
child: _isResting child: _isResting
? _buildRestScreen() // Rest Screen überdeckt das Gym (oder man macht ihn auch transparent) ? _buildRestScreen()
: _buildWorkoutScreen(currentExercise, currentSet, plateResult, : _buildWorkoutScreen(currentExercise, currentSet, plateResult,
completedHP, totalHP), completedHP, totalHP),
), ),
], ],
), ),
// body: _isResting
// ? _buildRestScreen()
// : _buildWorkoutScreen(
// currentExercise, currentSet, plateResult, completedHP, totalHP),
); );
} }
@ -568,7 +558,6 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
int completedHP, int completedHP,
int totalHP, int totalHP,
) { ) {
// Styles
final readableStyle = Theme.of(context).textTheme.bodyLarge?.copyWith( final readableStyle = Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.white, color: Colors.white,
shadows: [ shadows: [
@ -586,26 +575,20 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
return Column( return Column(
children: [ children: [
// --- 1. GEGNER BEREICH (Immersive) ---
// Wir nutzen Flexible, damit der Gegner Platz einnimmt, aber schrumpft, wenn nötig
Flexible( Flexible(
flex: 4, // Verhältnis zum unteren Teil flex: 4,
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
// Gegner Bild (Groß & Frei)
Padding( Padding(
padding: const EdgeInsets.only(bottom: 40), // Platz für HP Bar padding: const EdgeInsets.only(bottom: 40),
child: Image.asset( child: Image.asset(
_getEnemyAsset(currentExercise.exerciseId), _getEnemyAsset(currentExercise.exerciseId),
fit: BoxFit.contain, fit: BoxFit.contain,
// Ein Glow-Effekt hinter dem Gegner für bessere Abhebung vom Hintergrund
color: Colors.white.withOpacity(0.9), color: Colors.white.withOpacity(0.9),
colorBlendMode: BlendMode.modulate, colorBlendMode: BlendMode.modulate,
), ),
), ),
// Wave Badge (Oben Rechts, dezent)
Positioned( Positioned(
top: 16, top: 16,
right: 16, right: 16,
@ -626,15 +609,12 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
), ),
), ),
), ),
// HP Bar (Direkt unter dem Gegner, Schwebend)
Positioned( Positioned(
bottom: 10, bottom: 10,
left: 32, left: 32,
right: 32, right: 32,
child: Column( child: Column(
children: [ children: [
// Kleines Herz Icon
const Icon(Icons.favorite, const Icon(Icons.favorite,
color: AppTheme.errorColor, size: 24), color: AppTheme.errorColor, size: 24),
const SizedBox(height: 4), const SizedBox(height: 4),
@ -648,15 +628,11 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
], ],
), ),
), ),
// --- 2. KONTROLL BEREICH (Scrollable) ---
// Dieser Teil enthält die Trainings-Infos und den Counter
Expanded( Expanded(
flex: 6, flex: 6,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.surfaceColor color: AppTheme.surfaceColor.withOpacity(0.95),
.withOpacity(0.95), // Fast undurchsichtig für Lesbarkeit
borderRadius: borderRadius:
const BorderRadius.vertical(top: Radius.circular(24)), const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [ boxShadow: [
@ -670,8 +646,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
children: [ children: [
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(24, 24, 24, 100),
24, 24, 24, 100), // Unten Platz für Button
child: Column( child: Column(
children: [ children: [
Text( Text(
@ -692,10 +667,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
.titleMedium .titleMedium
?.copyWith(color: Colors.white70), ?.copyWith(color: Colors.white70),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Target Info (Kompakt)
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
@ -708,10 +680,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
'${currentSet.repsTarget}${currentSet.isAmrap ? "+" : ""}'), '${currentSet.repsTarget}${currentSet.isAmrap ? "+" : ""}'),
], ],
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Load / Assistance Visualizer
if (plateResult.bandAssistance != null) if (plateResult.bandAssistance != null)
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
@ -752,41 +721,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
isTwoSided: currentExercise.exerciseId == 'squat', isTwoSided: currentExercise.exerciseId == 'squat',
exerciseName: currentExercise.exerciseName, exerciseName: currentExercise.exerciseName,
), ),
const SizedBox(height: 32), 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<BattleScreen> {
), ),
), ),
), ),
// --- 3. FIXIERTER BUTTON ---
Container( Container(
color: color: AppTheme.surfaceColor,
AppTheme.surfaceColor, // Gleiche Farbe wie der Kontroll-Container
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: SafeArea( child: SafeArea(
top: false, top: false,
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
// onPressed: _repsCompleted >= currentSet.repsTarget
// ? _completeSet
// : null,
onPressed: () => _handleCompletePress(currentSet), onPressed: () => _handleCompletePress(currentSet),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
@ -834,7 +763,6 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
if (currentSet.isAmrap) { if (currentSet.isAmrap) {
_showAmrapDialog(currentSet); _showAmrapDialog(currentSet);
} else { } else {
// Standard-Satz: Wir gehen davon aus, dass das Ziel erreicht wurde
setState(() { setState(() {
_repsCompleted = currentSet.repsTarget; _repsCompleted = currentSet.repsTarget;
}); });
@ -849,7 +777,6 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
} }
void _showAmrapDialog(WorkoutSet set) { void _showAmrapDialog(WorkoutSet set) {
// Startwert ist das Ziel (oder was bisher eingestellt war)
int tempReps = set.repsTarget; int tempReps = set.repsTarget;
showModalBottomSheet( showModalBottomSheet(
@ -860,8 +787,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
borderRadius: BorderRadius.vertical(top: Radius.circular(24)), borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
), ),
builder: (context) { builder: (context) {
return StatefulBuilder(// Wichtig für State im Dialog return StatefulBuilder(builder: (context, setModalState) {
builder: (context, setModalState) {
return Padding( return Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Column( child: Column(
@ -880,8 +806,6 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
style: TextStyle(color: Colors.grey), style: TextStyle(color: Colors.grey),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Großer Counter
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -906,16 +830,13 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
onTap: () => setModalState(() => tempReps++)), onTap: () => setModalState(() => tempReps++)),
], ],
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
height: 56, height: 56,
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
Navigator.pop(context); // Dialog schließen Navigator.pop(context);
// Wert übernehmen und Satz beenden
setState(() { setState(() {
_repsCompleted = tempReps; _repsCompleted = tempReps;
}); });
@ -930,7 +851,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
fontSize: 18, fontWeight: FontWeight.bold)), 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 { class _CounterButton extends StatelessWidget {
final IconData icon; final IconData icon;
final VoidCallback? onTap; final VoidCallback? onTap;

View file

@ -45,10 +45,8 @@ class EnemyHPBar extends StatelessWidget {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Stack( Stack(
children: [ children: [
// Background
Container( Container(
height: 24, height: 24,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -60,8 +58,6 @@ class EnemyHPBar extends StatelessWidget {
), ),
), ),
), ),
// HP Fill
FractionallySizedBox( FractionallySizedBox(
widthFactor: percentage.clamp(0.0, 1.0), widthFactor: percentage.clamp(0.0, 1.0),
child: Container( child: Container(
@ -89,4 +85,3 @@ class EnemyHPBar extends StatelessWidget {
); );
} }
} }

View file

@ -77,14 +77,11 @@ class PlateVisualizer extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Left collar
Container( Container(
width: 8, width: 8,
height: 80, height: 80,
color: Colors.grey[800], color: Colors.grey[800],
), ),
// Plates (from largest to smallest)
...plateConfiguration.map((weight) { ...plateConfiguration.map((weight) {
final size = _getPlateSize(weight); final size = _getPlateSize(weight);
return Container( return Container(
@ -98,8 +95,6 @@ class PlateVisualizer extends StatelessWidget {
), ),
); );
}).toList(), }).toList(),
// Sleeve (bar end)
Container( Container(
width: 40, width: 40,
height: 20, height: 20,
@ -155,7 +150,6 @@ class PlateVisualizer extends StatelessWidget {
} }
double _getPlateSize(double weight) { double _getPlateSize(double weight) {
// Scale plate size based on weight
if (weight >= 20) return 120.0; if (weight >= 20) return 120.0;
if (weight >= 10) return 100.0; if (weight >= 10) return 100.0;
if (weight >= 5) return 80.0; if (weight >= 5) return 80.0;

View file

@ -5,22 +5,21 @@ part 'user_collection.g.dart';
@collection @collection
class UserCollection { class UserCollection {
Id id = Isar.autoIncrement; Id id = Isar.autoIncrement;
@Index(unique: true) @Index(unique: true)
String? serverId; // PocketBase ID String? serverId;
String email = ''; String email = '';
int xp = 0; int xp = 0;
int level = 1; int level = 1;
double currentBodyweight = 70.0; double currentBodyweight = 70.0;
String? inventorySettingsJson; // JSON string String? inventorySettingsJson;
String? avatarConfigJson; // JSON string String? avatarConfigJson;
DateTime? lastSyncAt; DateTime? lastSyncAt;
bool isDirty = false; // Needs sync bool isDirty = false;
DateTime createdAt = DateTime.now(); DateTime createdAt = DateTime.now();
DateTime updatedAt = DateTime.now(); DateTime updatedAt = DateTime.now();
} }

View file

@ -27,7 +27,6 @@ class ApiClient {
), ),
); );
// Add interceptors
_dio.interceptors.add( _dio.interceptors.add(
PrettyDioLogger( PrettyDioLogger(
requestHeader: true, requestHeader: true,
@ -39,7 +38,6 @@ class ApiClient {
), ),
); );
// Auth token interceptor
_dio.interceptors.add( _dio.interceptors.add(
InterceptorsWrapper( InterceptorsWrapper(
onRequest: (options, handler) async { onRequest: (options, handler) async {
@ -60,7 +58,6 @@ class ApiClient {
); );
} }
// Authentication
Future<Map<String, dynamic>> login(String email, String password) async { Future<Map<String, dynamic>> login(String email, String password) async {
try { try {
final response = await _dio.post( final response = await _dio.post(
@ -120,7 +117,6 @@ class ApiClient {
await _storage.delete(key: AppConstants.keyUserId); await _storage.delete(key: AppConstants.keyUserId);
} }
// Sync
Future<Map<String, dynamic>> sync({ Future<Map<String, dynamic>> sync({
required String lastSyncTimestamp, required String lastSyncTimestamp,
required Map<String, dynamic> pushData, required Map<String, dynamic> pushData,
@ -140,7 +136,6 @@ class ApiClient {
} }
} }
// Cycle Management
Future<Map<String, dynamic>> createCycle( Future<Map<String, dynamic>> createCycle(
Map<String, double> trainingMaxes) async { Map<String, double> trainingMaxes) async {
try { try {
@ -178,7 +173,6 @@ class ApiClient {
} }
} }
// Stats
Future<Map<String, dynamic>> getStatsHistory({ Future<Map<String, dynamic>> getStatsHistory({
required String exercise, required String exercise,
required String range, required String range,
@ -208,7 +202,6 @@ class ApiClient {
} }
} }
// Profile
Future<void> updateBodyweight(double bodyweight) async { Future<void> updateBodyweight(double bodyweight) async {
try { try {
await _dio.patch( await _dio.patch(
@ -240,7 +233,6 @@ class ApiClient {
required String newPasswordConfirm, required String newPasswordConfirm,
}) async { }) async {
try { try {
// PocketBase erwartet oldPassword, password, passwordConfirm
await _dio.patch( await _dio.patch(
'${ApiEndpoints.userUpdate}/$userId', '${ApiEndpoints.userUpdate}/$userId',
data: { data: {

View file

@ -32,21 +32,15 @@ class SyncService {
try { try {
debugPrint('🔄 Starting Sync...'); debugPrint('🔄 Starting Sync...');
// ---------------------------------------------------------
// STEP 1: Sync Cycles First (Parents of Workouts)
// ---------------------------------------------------------
final dirtyCycles = final dirtyCycles =
await isar.cycleCollections.filter().isDirtyEqualTo(true).findAll(); await isar.cycleCollections.filter().isDirtyEqualTo(true).findAll();
for (var cycle in dirtyCycles) { for (var cycle in dirtyCycles) {
try { try {
if (cycle.serverId == null) { if (cycle.serverId == null) {
// Create new cycle on server
debugPrint( debugPrint(
'📤 Pushing new cycle ${cycle.cycleNumber} to server...'); '📤 Pushing new cycle ${cycle.cycleNumber} to server...');
// Parse TMs safely
Map<String, double> tmsMap = {}; Map<String, double> tmsMap = {};
try { try {
final tms = jsonDecode(cycle.trainingMaxesJson); final tms = jsonDecode(cycle.trainingMaxesJson);
@ -54,7 +48,6 @@ class SyncService {
tms.map((k, v) => MapEntry(k, (v as num).toDouble()))); tms.map((k, v) => MapEntry(k, (v as num).toDouble())));
} catch (e) { } catch (e) {
debugPrint('⚠️ Error parsing TMs for cycle ${cycle.id}: $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}; tmsMap = {'squat': 0.0, 'pullup': 0.0, 'dip': 0.0};
} }
@ -62,13 +55,10 @@ class SyncService {
final newServerId = response['id']; final newServerId = response['id'];
await isar.writeTxn(() async { await isar.writeTxn(() async {
// Update cycle with server ID
cycle.serverId = newServerId; cycle.serverId = newServerId;
cycle.isDirty = false; cycle.isDirty = false;
await isar.cycleCollections.put(cycle); 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 oldLocalIdRef = cycle.id.toString();
final orphanWorkouts = await isar.workoutCollections final orphanWorkouts = await isar.workoutCollections
@ -77,15 +67,13 @@ class SyncService {
.findAll(); .findAll();
for (var w in orphanWorkouts) { for (var w in orphanWorkouts) {
w.cycleId = newServerId; // Update link to valid server ID w.cycleId = newServerId;
w.isDirty = true; // Ensure it gets picked up in next step w.isDirty = true;
await isar.workoutCollections.put(w); await isar.workoutCollections.put(w);
debugPrint('🔗 Relinked workout ${w.id} to cycle $newServerId'); debugPrint('🔗 Relinked workout ${w.id} to cycle $newServerId');
} }
}); });
} else { } 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 { await isar.writeTxn(() async {
cycle.isDirty = false; cycle.isDirty = false;
await isar.cycleCollections.put(cycle); await isar.cycleCollections.put(cycle);
@ -93,16 +81,10 @@ class SyncService {
} }
} catch (e) { } catch (e) {
debugPrint('❌ Failed to sync cycle: $e'); debugPrint('❌ Failed to sync cycle: $e');
// We stop here because workouts depend on cycles.
return; return;
} }
} }
// ---------------------------------------------------------
// STEP 2: Sync Workouts & User Stats
// ---------------------------------------------------------
// 1. Gather local changes
final dirtyUser = final dirtyUser =
await isar.userCollections.filter().isDirtyEqualTo(true).findFirst(); await isar.userCollections.filter().isDirtyEqualTo(true).findFirst();
@ -112,18 +94,14 @@ class SyncService {
if (dirtyUser == null && dirtyWorkouts.isEmpty) { if (dirtyUser == null && dirtyWorkouts.isEmpty) {
debugPrint('✅ Nothing to push.'); debugPrint('✅ Nothing to push.');
} else { } else {
// 2. Prepare Push Data
final pushData = <String, dynamic>{ final pushData = <String, dynamic>{
'workouts': dirtyWorkouts.where((w) { '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; return w.cycleId.length > 5;
}).map((w) { }).map((w) {
return { return {
'id': w.serverId, 'id': w.serverId,
'local_id': w.id, 'local_id': w.id,
'cycle_id': w.cycleId, // Must be a Server ID 'cycle_id': w.cycleId,
'week': w.week, 'week': w.week,
'day': w.day, 'day': w.day,
'completed_at': w.completedAt?.toIso8601String(), 'completed_at': w.completedAt?.toIso8601String(),
@ -141,16 +119,13 @@ class SyncService {
: null, : null,
}; };
// If we filtered out workouts, log it
if ((pushData['workouts'] as List).length < dirtyWorkouts.length) { if ((pushData['workouts'] as List).length < dirtyWorkouts.length) {
debugPrint( debugPrint(
'⚠️ Skipped some workouts because they lack a valid server cycle ID.'); '⚠️ Skipped some workouts because they lack a valid server cycle ID.');
} }
// 3. Get Last Sync Timestamp
final lastSync = await _storage.read(key: AppConstants.keyLastSync); final lastSync = await _storage.read(key: AppConstants.keyLastSync);
// 4. Call API
if ((pushData['workouts'] as List).isNotEmpty || if ((pushData['workouts'] as List).isNotEmpty ||
pushData['user_stats'] != null) { pushData['user_stats'] != null) {
debugPrint('📤 Pushing data...'); debugPrint('📤 Pushing data...');
@ -159,25 +134,17 @@ class SyncService {
pushData: pushData, pushData: pushData,
); );
// 5. Process Response
await isar.writeTxn(() async { await isar.writeTxn(() async {
// Update User
if (dirtyUser != null) { if (dirtyUser != null) {
dirtyUser.isDirty = false; dirtyUser.isDirty = false;
await isar.userCollections.put(dirtyUser); await isar.userCollections.put(dirtyUser);
} }
// Update pushed workouts (Clear dirty flags)
for (var w in dirtyWorkouts) { 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; w.isDirty = false;
await isar.workoutCollections.put(w); await isar.workoutCollections.put(w);
} }
// Process Pulled Workouts (Updates from Server)
if (response['pull_data'] != null && if (response['pull_data'] != null &&
response['pull_data']['workouts'] != null) { response['pull_data']['workouts'] != null) {
final pulledWorkouts = response['pull_data']['workouts'] as List; final pulledWorkouts = response['pull_data']['workouts'] as List;
@ -207,7 +174,6 @@ class SyncService {
} }
}); });
// 6. Save new Sync Timestamp
if (response['server_timestamp'] != null) { if (response['server_timestamp'] != null) {
await _storage.write( await _storage.write(
key: AppConstants.keyLastSync, key: AppConstants.keyLastSync,

View file

@ -88,14 +88,10 @@ class CycleRepository {
final cycleIdRef = currentCycle.serverId ?? currentCycle.id.toString(); 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 final completedMainWorkouts = await isar.workoutCollections
.filter() .filter()
.weekLessThan( .weekLessThan(4)
4) // Nur Woche 1-3 zählen (Deload Woche 4 ist optional für Finish) .completedAtIsNotNull()
.completedAtIsNotNull() // Nur abgeschlossene zählen
.group((q) => q .group((q) => q
.cycleIdEqualTo(cycleIdRef) .cycleIdEqualTo(cycleIdRef)
.or() .or()
@ -116,8 +112,6 @@ class CycleRepository {
'dip': (currentTMs['dip'] as num?)?.toDouble() ?? 0.0, 'dip': (currentTMs['dip'] as num?)?.toDouble() ?? 0.0,
}; };
// final cycleIdRef = currentCycle.serverId ?? currentCycle.id.toString();
final week3Workouts = await isar.workoutCollections final week3Workouts = await isar.workoutCollections
.filter() .filter()
.weekEqualTo(3) .weekEqualTo(3)

View file

@ -191,60 +191,27 @@ class UserRepository {
if (user?.serverId != null) { if (user?.serverId != null) {
await apiClient.deleteAccount(user!.serverId!); await apiClient.deleteAccount(user!.serverId!);
} }
// Lokal alles löschen
await logout(); await logout();
} }
// Future<void> 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<void> resetProgress() async { Future<void> resetProgress() async {
final user = await getLocalUser(); final user = await getLocalUser();
if (user != null) { if (user != null) {
// 1. SERVER RESET (Zwingend zuerst!)
try { try {
// Wir versuchen, den Server zu bereinigen.
await apiClient.resetProgress(); await apiClient.resetProgress();
} catch (e) { } 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( throw Exception(
"Server connection required to reset progress. Please try again when online."); "Server connection required to reset progress. Please try again when online.");
} }
// 2. LOKALER RESET (Nur wenn Server erfolgreich war)
user.xp = 0; user.xp = 0;
user.level = 1; 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; user.isDirty = false;
await isar.writeTxn(() async { await isar.writeTxn(() async {
// User speichern
await isar.userCollections.put(user); await isar.userCollections.put(user);
// Alle lokalen Trainingsdaten löschen
await isar.cycleCollections.clear(); await isar.cycleCollections.clear();
await isar.workoutCollections.clear(); await isar.workoutCollections.clear();
}); });

View file

@ -74,21 +74,9 @@ class WorkoutRepository {
await saveWorkout(workout); await saveWorkout(workout);
} }
// Future<WorkoutCollection?> getWorkoutByWeekDay({
// required String cycleId,
// required int week,
// required int day,
// }) async {
// return await isar.workoutCollections
// .filter()
// .cycleIdEqualTo(cycleId)
// .weekEqualTo(week)
// .dayEqualTo(day)
// .findFirst();
// }
Future<WorkoutCollection?> getWorkoutByWeekDay({ Future<WorkoutCollection?> getWorkoutByWeekDay({
required String cycleId, // Meist Server ID required String cycleId,
String? localCycleId, // NEU: Backup Local ID String? localCycleId,
required int week, required int week,
required int day, required int day,
}) async { }) async {
@ -97,7 +85,6 @@ class WorkoutRepository {
.weekEqualTo(week) .weekEqualTo(week)
.dayEqualTo(day) .dayEqualTo(day)
.group((q) { .group((q) {
// Wir suchen ENTWEDER nach der Server-ID ODER nach der lokalen ID
var query = q.cycleIdEqualTo(cycleId); var query = q.cycleIdEqualTo(cycleId);
if (localCycleId != null) { if (localCycleId != null) {
query = query.or().cycleIdEqualTo(localCycleId); query = query.or().cycleIdEqualTo(localCycleId);

View file

@ -1,169 +1,9 @@
// import 'dart:math';
// class PlateLoadResult {
// final bool success;
// final List<double> 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<double> availablePlates,
// Map<String, double> 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<double>.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<double> plates) {
// final loaded = <double>[];
// 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<double> plates;
// final double totalWeight;
// _FitResult({
// required this.success,
// required this.plates,
// required this.totalWeight,
// });
// }
import 'dart:math'; import 'dart:math';
class PlateLoadResult { class PlateLoadResult {
final bool success; final bool success;
final List<double> plateConfiguration; final List<double> plateConfiguration;
final String? bandAssistance; // Name of the band needed final String? bandAssistance;
final double totalAchieved; final double totalAchieved;
final String message; final String message;
@ -193,14 +33,12 @@ class PlateCalculator {
}) { }) {
double needed = targetWeight - barWeight; double needed = targetWeight - barWeight;
// 1. Handle Assistance (Negative weight needed)
if (needed < 0 && !isTwoSided) { if (needed < 0 && !isTwoSided) {
final deficit = needed.abs(); final deficit = needed.abs();
String? bestBand; String? bestBand;
double closestResistance = 0.0; double closestResistance = 0.0;
double minDiff = double.infinity; double minDiff = double.infinity;
// Finde das am besten passende Band
availableBands.forEach((name, resistance) { availableBands.forEach((name, resistance) {
final diff = (resistance - deficit).abs(); final diff = (resistance - deficit).abs();
if (diff < minDiff) { if (diff < minDiff) {
@ -233,19 +71,15 @@ class PlateCalculator {
); );
} }
// Band gefunden und es ist sinnvoll
return PlateLoadResult( return PlateLoadResult(
success: true, success: true,
plateConfiguration: [], plateConfiguration: [],
bandAssistance: bestBand, bandAssistance: bestBand,
// Wir geben hier das echte erreichte Gewicht an (Körpergewicht - Bandstärke)
totalAchieved: barWeight - closestResistance, totalAchieved: barWeight - closestResistance,
message: 'Use $bestBand band for assistance', 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) { if (needed <= 0.1) {
return PlateLoadResult( return PlateLoadResult(
success: true, success: true,
@ -255,7 +89,6 @@ class PlateCalculator {
); );
} }
// Sort plates descending to find smallest plate later
final sortedPlates = List<double>.from(availablePlates) final sortedPlates = List<double>.from(availablePlates)
..sort((a, b) => b.compareTo(a)); ..sort((a, b) => b.compareTo(a));
@ -268,11 +101,9 @@ class PlateCalculator {
); );
} }
// ROUNDING LOGIC (wie vorher besprochen)
final smallestPlate = sortedPlates.last; final smallestPlate = sortedPlates.last;
final targetPerSideRaw = isTwoSided ? needed / 2 : needed; final targetPerSideRaw = isTwoSided ? needed / 2 : needed;
// Round to nearest smallest plate
final roundedPerSide = final roundedPerSide =
(targetPerSideRaw / smallestPlate).round() * smallestPlate; (targetPerSideRaw / smallestPlate).round() * smallestPlate;
@ -285,7 +116,6 @@ class PlateCalculator {
); );
} }
// Try to fit the ROUNDED weight
final result = _greedyFit(roundedPerSide, sortedPlates); final result = _greedyFit(roundedPerSide, sortedPlates);
if (!result.success) { if (!result.success) {
@ -308,7 +138,6 @@ class PlateCalculator {
); );
} }
/// Greedy algorithm to fit plates
static _FitResult _greedyFit(double target, List<double> plates) { static _FitResult _greedyFit(double target, List<double> plates) {
final loaded = <double>[]; final loaded = <double>[];
var remaining = target; var remaining = target;

View file

@ -285,10 +285,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_chart name: fl_chart
sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.66.2" version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -306,10 +306,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "6.0.0"
flutter_riverpod: flutter_riverpod:
dependency: "direct main" dependency: "direct main"
description: description:
@ -420,10 +420,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.8.1" version: "17.0.0"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
@ -468,10 +468,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.20.2"
io: io:
dependency: transitive dependency: transitive
description: description:
@ -556,10 +556,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "6.0.0"
logger: logger:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -35,19 +35,19 @@ dependencies:
shimmer: ^3.0.0 shimmer: ^3.0.0
# Utilities # Utilities
intl: ^0.19.0 intl: ^0.20.2
freezed_annotation: ^2.4.1 freezed_annotation: ^2.4.1
json_annotation: ^4.9.0 json_annotation: ^4.9.0
equatable: ^2.0.5 equatable: ^2.0.5
logger: ^2.3.0 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: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^3.0.0 flutter_lints: ^6.0.0
# Code Generation # Code Generation
build_runner: ^2.4.9 build_runner: ^2.4.9