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