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

@ -23,9 +23,9 @@ class CodexScreen extends StatelessWidget {
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.',
'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,
@ -35,9 +35,9 @@ class CodexScreen extends StatelessWidget {
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.',
'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,
@ -47,9 +47,9 @@ class CodexScreen extends StatelessWidget {
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.',
'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

@ -7,20 +7,19 @@ 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