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

View file

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

View file

@ -86,7 +86,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
),
const SizedBox(height: 24),
// Title
Text(
'WELCOME BACK',
style: Theme.of(context).textTheme.displayMedium,
@ -100,7 +99,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
),
const SizedBox(height: 48),
// Email Field
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
@ -120,7 +118,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
),
const SizedBox(height: 16),
// Password Field
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
@ -150,7 +147,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
),
const SizedBox(height: 32),
// Login Button
ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
child: _isLoading
@ -166,7 +162,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
),
const SizedBox(height: 16),
// Register Link
Row(
mainAxisAlignment: MainAxisAlignment.center,
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/presentation/widgets/avatar_editor.dart';
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
import 'dart:convert'; // Für jsonDecode
import 'dart:convert';
class ProfileScreen extends ConsumerStatefulWidget {
const ProfileScreen({super.key});
@ -39,14 +39,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
});
}
}
// Future<void> _loadUser() async {
// final user = await ref.read(userRepositoryProvider).getLocalUser();
// if (user != null && mounted) {
// setState(() {
// _currentBodyweight = user.currentBodyweight;
// });
// }
// }
Future<void> _saveBodyweight() async {
setState(() => _isLoading = true);
@ -177,7 +169,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
showModalBottomSheet(
context: context,
isScrollControlled: true, // Wichtig für Fullscreen-Feeling
isScrollControlled: true,
useSafeArea: true,
backgroundColor: AppTheme.backgroundColor,
builder: (context) => Scaffold(
@ -190,10 +182,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
actions: [
TextButton(
onPressed: () {
// Speichern wird hier ausgelöst durch den Save-Callback im Editor Wrapper
// Aber da der Editor im BottomSheet state hat, müssen wir die Config rausbekommen.
// Einfacher: Wir wrappen den Editor in ein Stateful Widget im Dialog oder übergeben einen Callback.
// Da wir hier im ProfileScreen sind, können wir eine temporäre Variable nutzen und beim Schließen speichern.
Navigator.pop(context, _tempAvatarConfig);
},
child: const Text('SAVE',
@ -205,14 +193,12 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
),
body: AvatarEditor(
initialConfig: currentConfig,
onChanged: (conf) => _tempAvatarConfig =
conf, // _tempAvatarConfig muss in State definiert werden
onChanged: (conf) => _tempAvatarConfig = conf,
),
),
).then((result) async {
if (result is AvatarConfig) {
setState(() => _isLoading = true);
// Speichern
_user!.avatarConfigJson = jsonEncode(result.toJson());
_user!.isDirty = true;
await ref.read(userRepositoryProvider).saveLocalUser(_user!);
@ -267,13 +253,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
child: IconButton(
icon: const Icon(Icons.edit, size: 16),
onPressed: _showAvatarEditor,
// onPressed: () {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text(
// 'Avatar customization coming soon!')),
// );
// },
),
),
),
@ -281,8 +260,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
),
),
const SizedBox(height: 32),
// Bodyweight Section
Text('Physical Stats',
style: Theme.of(context)
.textTheme
@ -330,8 +307,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
),
),
const SizedBox(height: 32),
// Account Actions
Text('Account Security',
style: Theme.of(context)
.textTheme
@ -345,8 +320,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
onTap: _showChangePasswordDialog,
),
const Divider(),
// Danger Zone
const SizedBox(height: 24),
Text('Danger Zone',
style: Theme.of(context)
@ -414,7 +387,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
),
),
const SizedBox(height: 32),
OutlinedButton.icon(
onPressed: () async {
await userRepo.logout();

View file

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

View file

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

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

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

View file

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

View file

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

View file

@ -30,7 +30,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
1.25: 2,
};
// Band selection state - Default configuration based on standard colors
final Map<String, bool> _bandInventory = {
'Blue': true,
'Green': true,
@ -81,7 +80,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
}
void _handleNext() {
// Listen bauen (wie vorher)
final platesList = <double>[];
_plateInventory.forEach((weight, count) {
for (int i = 0; i < count; i++) platesList.add(weight);
@ -104,13 +102,12 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
'bands': bandsList,
};
// Im Provider speichern für den nächsten Screen
ref.read(onboardingDataProvider.notifier).update((state) => {
...state,
'inventory_settings': inventorySettings,
});
context.push('/onboarding/avatar'); // Neue Route!
context.push('/onboarding/avatar');
}
Future<void> _handleFinish() async {
@ -120,7 +117,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
final onboardingData = ref.read(onboardingDataProvider);
final userRepo = ref.read(userRepositoryProvider);
// Build plates list for DB
final platesList = <double>[];
_plateInventory.forEach((weight, count) {
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>>[];
_bandInventory.forEach((color, isSelected) {
if (isSelected) {
@ -146,7 +141,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
'bands': bandsList,
};
// Register user with all data
final user = await userRepo.register(
email: onboardingData['email'] ?? '',
password: onboardingData['password'] ?? '',
@ -156,7 +150,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
debugPrint('✅ User registered: ${user.serverId}');
// Create first cycle
final trainingMaxes =
onboardingData['training_maxes'] as Map<String, dynamic>?;
@ -174,10 +167,8 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
}
if (mounted) {
// Clear onboarding data
ref.read(onboardingDataProvider.notifier).state = {};
// Navigate to hub
context.go('/hub');
}
} catch (e, stackTrace) {
@ -186,7 +177,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
if (mounted) {
String message = 'Setup failed: ${e.toString()}';
// Catch unique constraint error (PocketBase returns 400 usually)
if (e.toString().toLowerCase().contains('unique') ||
e.toString().toLowerCase().contains('email')) {
message = 'Email already exists. Please login or use another email.';
@ -238,15 +228,12 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Progress Indicator
LinearProgressIndicator(
value: 0.75,
backgroundColor: AppTheme.xpBarBackground,
color: AppTheme.primaryColor,
),
const SizedBox(height: 32),
// Title
Text(
'Equipment Inventory',
style: Theme.of(context).textTheme.displayMedium,
@ -257,8 +244,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
// Bar Weight Selector
Text(
'Barbell Weight',
style: Theme.of(context)
@ -286,8 +271,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Presets
Text(
'Quick Presets',
style: Theme.of(context)
@ -321,8 +304,6 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
],
),
const SizedBox(height: 32),
// Plate Selection
Text(
'Available Plates',
style: Theme.of(context)
@ -342,9 +323,7 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
},
);
}).toList(),
const SizedBox(height: 32),
Text(
'Resistance Bands (Assistance)',
style: Theme.of(context)
@ -385,27 +364,11 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
);
}).toList(),
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _handleNext,
child: const Text('NEXT STEP'),
),
// // Finish Button
// ElevatedButton(
// onPressed: _isLoading ? null : _handleFinish,
// child: _isLoading
// ? const SizedBox(
// height: 20,
// width: 20,
// child: CircularProgressIndicator(
// strokeWidth: 2,
// color: Colors.black,
// ),
// )
// : const Text('FINISH SETUP'),
// ),
],
),
),
@ -413,88 +376,3 @@ class _InventorySetupScreenState extends ConsumerState<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> {
final _formKey = GlobalKey<FormState>();
// Controllers for each exercise
final _squatWeightController = TextEditingController(text: '100');
final _squatRepsController = TextEditingController(text: '5');
final _pullupWeightController = TextEditingController(text: '0');
@ -48,20 +47,17 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
void _calculateAll() {
final bodyweight = ref.read(onboardingDataProvider)['bodyweight'] ?? 80.0;
// Squat
final squatWeight = double.tryParse(_squatWeightController.text) ?? 0;
final squatReps = int.tryParse(_squatRepsController.text) ?? 1;
final squat1RM = WendlerCalculator.calculate1RM(squatWeight, squatReps);
final squatTM = WendlerCalculator.calculateTrainingMax(squat1RM);
// Pullup (bodyweight + additional weight)
final pullupAdditional = double.tryParse(_pullupWeightController.text) ?? 0;
final pullupReps = int.tryParse(_pullupRepsController.text) ?? 1;
final pullupTotal = bodyweight + pullupAdditional;
final pullup1RM = WendlerCalculator.calculate1RM(pullupTotal, pullupReps);
final pullupTM = WendlerCalculator.calculateTrainingMax(pullup1RM);
// Dip (bodyweight + additional weight)
final dipAdditional = double.tryParse(_dipWeightController.text) ?? 0;
final dipReps = int.tryParse(_dipRepsController.text) ?? 1;
final dipTotal = bodyweight + dipAdditional;
@ -85,7 +81,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
void _handleContinue() {
if (!_formKey.currentState!.validate()) return;
// Store training maxes
ref.read(onboardingDataProvider.notifier).update((state) => {
...state,
'training_maxes': _calculatedTMs,
@ -114,17 +109,14 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Progress Indicator
LinearProgressIndicator(
value: 0.5,
backgroundColor: AppTheme.xpBarBackground,
color: AppTheme.primaryColor,
),
const SizedBox(height: 32),
// Title
Text(
'Combat Calibration', // Statt "Strength Assessment"
'Combat Calibration',
style: Theme.of(context).textTheme.displayMedium,
),
const SizedBox(height: 8),
@ -132,18 +124,12 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
'We need to assess your current power level to assign the correct monsters.', // Flavor
style: Theme.of(context).textTheme.bodyMedium,
),
// Text(
// 'Strength Assessment',
// style: Theme.of(context).textTheme.displayMedium,
// ),
const SizedBox(height: 8),
Text(
'Enter your recent best performance for each exercise',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
// Squat
_ExerciseCard(
exerciseName: 'Back Squat',
icon: Icons.accessibility_new,
@ -155,8 +141,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
onChanged: _calculateAll,
),
const SizedBox(height: 16),
// Pullup
_ExerciseCard(
exerciseName: 'Weighted Pull-up',
icon: Icons.north,
@ -169,8 +153,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
onChanged: _calculateAll,
),
const SizedBox(height: 16),
// Dip
_ExerciseCard(
exerciseName: 'Weighted Dip',
icon: Icons.south,
@ -183,8 +165,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
onChanged: _calculateAll,
),
const SizedBox(height: 32),
// Info Box
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
@ -202,10 +182,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
),
const SizedBox(width: 12),
Expanded(
// child: Text(
// 'Training Max (TM) = 90% of your estimated 1RM. This is what we\'ll use for programming.',
// style: Theme.of(context).textTheme.bodySmall,
// ),
child: Text(
'Your "Training Max" (TM) is your base combat power. We calculate it as 90% of your max potential to ensure long-term survival.', // Flavor
style: Theme.of(context)
@ -218,8 +194,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
),
),
const SizedBox(height: 32),
// Continue Button
ElevatedButton(
onPressed: _handleContinue,
child: const Text('CONTINUE'),
@ -285,7 +259,6 @@ class _ExerciseCard extends StatelessWidget {
),
const SizedBox(height: 16),
// Input Fields
Row(
children: [
Expanded(
@ -341,7 +314,6 @@ class _ExerciseCard extends StatelessWidget {
),
const SizedBox(height: 16),
// Calculations
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(

View file

@ -7,115 +7,22 @@ import '../../../../core/constants/asset_paths.dart';
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// body: SafeArea(
// child: Padding(
// padding: const EdgeInsets.all(24),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// crossAxisAlignment: CrossAxisAlignment.stretch,
// children: [
// const Spacer(),
// // Logo
// Container(
// width: 120,
// height: 120,
// decoration: BoxDecoration(
// color: AppTheme.primaryColor,
// borderRadius: BorderRadius.circular(24),
// ),
// child: const Icon(
// Icons.fitness_center,
// size: 64,
// color: Colors.black,
// ),
// ),
// const SizedBox(height: 32),
// // Title
// Text(
// 'WELCOME TO',
// style: Theme.of(context).textTheme.headlineMedium,
// textAlign: TextAlign.center,
// ),
// Text(
// 'SLRPG',
// style: Theme.of(context).textTheme.displayLarge,
// textAlign: TextAlign.center,
// ),
// const SizedBox(height: 16),
// // Description
// Text(
// 'Transform your training into an epic RPG adventure',
// style: Theme.of(context).textTheme.bodyLarge,
// textAlign: TextAlign.center,
// ),
// const SizedBox(height: 48),
// // Features
// _FeatureItem(
// icon: Icons.trending_up,
// title: 'Progressive Overload',
// description: 'Wendler 5/3/1 periodization',
// ),
// const SizedBox(height: 16),
// _FeatureItem(
// icon: Icons.videogame_asset,
// title: 'Gamified Training',
// description: 'Level up, earn XP, unlock achievements',
// ),
// const SizedBox(height: 16),
// _FeatureItem(
// icon: Icons.offline_bolt,
// title: 'Offline First',
// description: 'Train anywhere, sync when ready',
// ),
// const Spacer(),
// // Continue Button
// ElevatedButton(
// onPressed: () => context.go('/onboarding/bodyweight'),
// child: const Text('GET STARTED'),
// ),
// const SizedBox(height: 16),
// // Skip to Login
// TextButton(
// onPressed: () => context.go('/login'),
// child: const Text('Already have an account? Login'),
// ),
// ],
// ),
// ),
// ),
// );
// }
// // ... imports
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// 1. Hintergrund (Street Park)
Positioned.fill(
child: Image.asset(
AssetPaths.bgStreetParkDay,
fit: BoxFit.cover,
),
),
// 2. Overlay (Dunkel für Lesbarkeit)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.7),
),
),
// 3. Inhalt
SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
@ -124,8 +31,6 @@ class WelcomeScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Spacer(),
// Logo (Optional: Kann bleiben oder weg)
Container(
width: 100,
height: 100,
@ -142,10 +47,8 @@ class WelcomeScreen extends StatelessWidget {
size: 56, color: Colors.black),
),
const SizedBox(height: 32),
// RPG Title
Text(
'ENTER THE ARENA', // Statt "WELCOME TO"
'ENTER THE ARENA',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Colors.white70,
letterSpacing: 2,
@ -164,8 +67,6 @@ class WelcomeScreen extends StatelessWidget {
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// RPG Description
const Text(
'The Iron Golems have awakened. The Gravity Demons are pulling the world into the abyss.\n\n'
'Only a true Streetlifter can stop them. Are you ready to forge your body into a weapon?',
@ -174,32 +75,25 @@ class WelcomeScreen extends StatelessWidget {
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
// Features (Umformuliert)
_FeatureItem(
icon: Icons.shield, // Statt trending_up
icon: Icons.shield,
title: 'Build Your Armor',
description: 'Progressive overload based on Wendler 5/3/1.',
),
const SizedBox(height: 16),
_FeatureItem(
icon: Icons.videogame_asset,
// icon: Icons
// .swords, // Statt videogame_asset (wenn Icon verfügbar, sonst Flash/Star)
title: 'Slay Monsters',
description:
'Turn every rep into damage against epic foes.',
),
const SizedBox(height: 16),
_FeatureItem(
icon: Icons.inventory_2, // Statt offline_bolt
icon: Icons.inventory_2,
title: 'Gather Loot',
description: 'Earn XP, level up, and unlock new gear.',
),
const Spacer(),
// Button
ElevatedButton(
onPressed: () => context.go('/onboarding/bodyweight'),
style: ElevatedButton.styleFrom(
@ -211,7 +105,6 @@ class WelcomeScreen extends StatelessWidget {
fontWeight: FontWeight.bold, letterSpacing: 1)),
),
const SizedBox(height: 16),
TextButton(
onPressed: () => context.go('/login'),
child: const Text('Already a hero? Login here',

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,21 +32,15 @@ class SyncService {
try {
debugPrint('🔄 Starting Sync...');
// ---------------------------------------------------------
// STEP 1: Sync Cycles First (Parents of Workouts)
// ---------------------------------------------------------
final dirtyCycles =
await isar.cycleCollections.filter().isDirtyEqualTo(true).findAll();
for (var cycle in dirtyCycles) {
try {
if (cycle.serverId == null) {
// Create new cycle on server
debugPrint(
'📤 Pushing new cycle ${cycle.cycleNumber} to server...');
// Parse TMs safely
Map<String, double> tmsMap = {};
try {
final tms = jsonDecode(cycle.trainingMaxesJson);
@ -54,7 +48,6 @@ class SyncService {
tms.map((k, v) => MapEntry(k, (v as num).toDouble())));
} catch (e) {
debugPrint('⚠️ Error parsing TMs for cycle ${cycle.id}: $e');
// Default fallback if parsing fails
tmsMap = {'squat': 0.0, 'pullup': 0.0, 'dip': 0.0};
}
@ -62,13 +55,10 @@ class SyncService {
final newServerId = response['id'];
await isar.writeTxn(() async {
// Update cycle with server ID
cycle.serverId = newServerId;
cycle.isDirty = false;
await isar.cycleCollections.put(cycle);
// CRITICAL: Update all workouts that linked to the local ID of this cycle
// Since we stored 'local ID string' in cycleId for offline workouts
final oldLocalIdRef = cycle.id.toString();
final orphanWorkouts = await isar.workoutCollections
@ -77,15 +67,13 @@ class SyncService {
.findAll();
for (var w in orphanWorkouts) {
w.cycleId = newServerId; // Update link to valid server ID
w.isDirty = true; // Ensure it gets picked up in next step
w.cycleId = newServerId;
w.isDirty = true;
await isar.workoutCollections.put(w);
debugPrint('🔗 Relinked workout ${w.id} to cycle $newServerId');
}
});
} else {
// Cycle already has server ID but marked dirty -> Update on server if needed
// For MVP we assume cycles are immutable except for status, skipping update logic to avoid complexity
await isar.writeTxn(() async {
cycle.isDirty = false;
await isar.cycleCollections.put(cycle);
@ -93,16 +81,10 @@ class SyncService {
}
} catch (e) {
debugPrint('❌ Failed to sync cycle: $e');
// We stop here because workouts depend on cycles.
return;
}
}
// ---------------------------------------------------------
// STEP 2: Sync Workouts & User Stats
// ---------------------------------------------------------
// 1. Gather local changes
final dirtyUser =
await isar.userCollections.filter().isDirtyEqualTo(true).findFirst();
@ -112,18 +94,14 @@ class SyncService {
if (dirtyUser == null && dirtyWorkouts.isEmpty) {
debugPrint('✅ Nothing to push.');
} else {
// 2. Prepare Push Data
final pushData = <String, dynamic>{
'workouts': dirtyWorkouts.where((w) {
// Filter out workouts that still don't have a valid cycle Server ID (e.g. if cycle sync failed)
// A valid PocketBase ID is 15 chars. A local ID is usually "1", "2".
// This is a heuristic check.
return w.cycleId.length > 5;
}).map((w) {
return {
'id': w.serverId,
'local_id': w.id,
'cycle_id': w.cycleId, // Must be a Server ID
'cycle_id': w.cycleId,
'week': w.week,
'day': w.day,
'completed_at': w.completedAt?.toIso8601String(),
@ -141,16 +119,13 @@ class SyncService {
: null,
};
// If we filtered out workouts, log it
if ((pushData['workouts'] as List).length < dirtyWorkouts.length) {
debugPrint(
'⚠️ Skipped some workouts because they lack a valid server cycle ID.');
}
// 3. Get Last Sync Timestamp
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
// 4. Call API
if ((pushData['workouts'] as List).isNotEmpty ||
pushData['user_stats'] != null) {
debugPrint('📤 Pushing data...');
@ -159,25 +134,17 @@ class SyncService {
pushData: pushData,
);
// 5. Process Response
await isar.writeTxn(() async {
// Update User
if (dirtyUser != null) {
dirtyUser.isDirty = false;
await isar.userCollections.put(dirtyUser);
}
// Update pushed workouts (Clear dirty flags)
for (var w in dirtyWorkouts) {
// We assume success if no error thrown
// Ideally we match IDs from response, but for MVP optimistically clearing is okay
// providing we don't overwrite serverId if it was null.
// The server usually returns the new/updated records in 'pull_data' anyway.
w.isDirty = false;
await isar.workoutCollections.put(w);
}
// Process Pulled Workouts (Updates from Server)
if (response['pull_data'] != null &&
response['pull_data']['workouts'] != null) {
final pulledWorkouts = response['pull_data']['workouts'] as List;
@ -207,7 +174,6 @@ class SyncService {
}
});
// 6. Save new Sync Timestamp
if (response['server_timestamp'] != null) {
await _storage.write(
key: AppConstants.keyLastSync,

View file

@ -88,14 +88,10 @@ class CycleRepository {
final cycleIdRef = currentCycle.serverId ?? currentCycle.id.toString();
// --- FIX START: Vollständigkeitsprüfung ---
// Wir zählen, wie viele Workouts in Woche 1, 2 und 3 tatsächlich abgeschlossen wurden.
// Es müssen genau 9 sein (3 Wochen * 3 Tage).
final completedMainWorkouts = await isar.workoutCollections
.filter()
.weekLessThan(
4) // Nur Woche 1-3 zählen (Deload Woche 4 ist optional für Finish)
.completedAtIsNotNull() // Nur abgeschlossene zählen
.weekLessThan(4)
.completedAtIsNotNull()
.group((q) => q
.cycleIdEqualTo(cycleIdRef)
.or()
@ -116,8 +112,6 @@ class CycleRepository {
'dip': (currentTMs['dip'] as num?)?.toDouble() ?? 0.0,
};
// final cycleIdRef = currentCycle.serverId ?? currentCycle.id.toString();
final week3Workouts = await isar.workoutCollections
.filter()
.weekEqualTo(3)

View file

@ -191,60 +191,27 @@ class UserRepository {
if (user?.serverId != null) {
await apiClient.deleteAccount(user!.serverId!);
}
// Lokal alles löschen
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 {
final user = await getLocalUser();
if (user != null) {
// 1. SERVER RESET (Zwingend zuerst!)
try {
// Wir versuchen, den Server zu bereinigen.
await apiClient.resetProgress();
} catch (e) {
// Wenn das fehlschlägt (z.B. Offline), brechen wir ab.
// Ein lokaler Reset ohne Server-Reset führt sonst zu Daten-Chaos beim nächsten Sync.
throw Exception(
"Server connection required to reset progress. Please try again when online.");
}
// 2. LOKALER RESET (Nur wenn Server erfolgreich war)
user.xp = 0;
user.level = 1;
// Wichtig: Wir setzen isDirty auf FALSE.
// Der Server weiß schon Bescheid (durch den API Call oben).
// Wir müssen ihm nicht nochmal sagen, dass XP jetzt 0 ist.
user.isDirty = false;
await isar.writeTxn(() async {
// User speichern
await isar.userCollections.put(user);
// Alle lokalen Trainingsdaten löschen
await isar.cycleCollections.clear();
await isar.workoutCollections.clear();
});

View file

@ -74,21 +74,9 @@ class WorkoutRepository {
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({
required String cycleId, // Meist Server ID
String? localCycleId, // NEU: Backup Local ID
required String cycleId,
String? localCycleId,
required int week,
required int day,
}) async {
@ -97,7 +85,6 @@ class WorkoutRepository {
.weekEqualTo(week)
.dayEqualTo(day)
.group((q) {
// Wir suchen ENTWEDER nach der Server-ID ODER nach der lokalen ID
var query = q.cycleIdEqualTo(cycleId);
if (localCycleId != null) {
query = query.or().cycleIdEqualTo(localCycleId);

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

View file

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

View file

@ -35,19 +35,19 @@ dependencies:
shimmer: ^3.0.0
# Utilities
intl: ^0.19.0
intl: ^0.20.2
freezed_annotation: ^2.4.1
json_annotation: ^4.9.0
equatable: ^2.0.5
logger: ^2.3.0
fl_chart: ^0.66.0
fl_chart: ^1.1.1
go_router: ^14.2.0
go_router: ^17.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter_lints: ^6.0.0
# Code Generation
build_runner: ^2.4.9