feat: Completed with Starter MainLifts

This commit is contained in:
Patryk Hegenberg 2025-12-07 12:03:42 +01:00
parent 311d764a4d
commit 2609446e9a
15 changed files with 642 additions and 491 deletions

View file

@ -44,6 +44,7 @@
<!-- </queries>--> <!-- </queries>-->
<!--</manifest>--> <!--</manifest>-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:label="streetlifting_rpg" android:label="streetlifting_rpg"
android:name="${applicationName}" android:name="${applicationName}"
@ -70,4 +71,4 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
</manifest> </manifest>

View file

@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
class AppConstants { class AppConstants {
// API Configuration // API Configuration
static const String apiBaseUrl = 'http://10.0.2.2:8090'; // Android emulator // static const String apiBaseUrl = 'http://10.0.2.2:8090'; // Android emulator
static const String apiBaseUrl = 'https://slift.patanix.de';
static const String apiVersion = 'v1'; static const String apiVersion = 'v1';
// Wendler 5/3/1 Constants // Wendler 5/3/1 Constants

View file

@ -1,10 +1,10 @@
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';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../../core/constants/asset_paths.dart'; // import '../../../../core/constants/asset_paths.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../shared/data/local/app_database.dart'; import '../../../../shared/data/local/app_database.dart';
import '../../../../shared/data/repositories/user_repository.dart'; import '../../../../shared/data/repositories/user_repository.dart';
@ -54,27 +54,64 @@ class _HubScreenState extends ConsumerState<HubScreen> {
required int day, required int day,
required Map<String, double> trainingMaxes, required Map<String, double> trainingMaxes,
required double bodyweight, required double bodyweight,
required UserCollection user,
}) { }) {
final exercises = <Exercise>[]; final exercises = <Exercise>[];
final variants = user.exerciseVariants ?? {};
void addExercise(String id, String name, ExerciseType type, bool isMain) { (String, String, ExerciseType) resolveVariant(String slot, String defaultId,
final tm = trainingMaxes[id] ?? 0.0; String defaultName, ExerciseType defaultType) {
final variant = variants[slot];
if (slot == 'pull') {
if (variant == 'row') return ('row', 'Pendlay Row', ExerciseType.row);
return ('pullup', 'Weighted Pull-up', ExerciseType.pullup);
}
if (slot == 'push') {
if (variant == 'bench') {
return ('bench', 'Bench Press', ExerciseType.bench);
}
return ('dip', 'Weighted Dip', ExerciseType.dip);
}
return (defaultId, defaultName, defaultType);
}
void addExercise(String slot, String defaultId, String defaultName,
ExerciseType defaultType, bool isMain) {
final (id, name, type) =
resolveVariant(slot, defaultId, defaultName, defaultType);
final tmKey = defaultId;
final tm = trainingMaxes[tmKey] ?? 0.0;
List<WorkoutSet> sets; List<WorkoutSet> sets;
if (isMain) { if (isMain) {
sets = WendlerCalculator.generateSets( if (type == ExerciseType.row || type == ExerciseType.bench) {
week: week, sets = WendlerCalculator.generateLinearSets(
trainingMax: tm, trainingMax: tm,
exerciseType: type, exerciseType: type,
currentBodyweight: bodyweight, currentBodyweight: user.currentBodyweight);
); } else {
sets = WendlerCalculator.generateSets(
week: week,
trainingMax: tm,
exerciseType: type,
currentBodyweight: user.currentBodyweight,
);
}
} else { } else {
if (week == 4) return; if (week == 4) {
return;
}
if (type == ExerciseType.row || type == ExerciseType.bench) return;
sets = WendlerCalculator.generateFSLSets( sets = WendlerCalculator.generateFSLSets(
trainingMax: tm, trainingMax: tm,
exerciseType: type, exerciseType: type,
currentBodyweight: bodyweight, currentBodyweight: user.currentBodyweight,
); );
} }
@ -82,21 +119,23 @@ class _HubScreenState extends ConsumerState<HubScreen> {
exercises.add(Exercise( exercises.add(Exercise(
exerciseId: id, exerciseId: id,
exerciseName: isMain ? name : '$name (FSL)', exerciseName: isMain ? name : '$name (FSL)',
bodyweightAtSession: bodyweight, bodyweightAtSession: user.currentBodyweight,
sets: sets, sets: sets,
)); ));
} }
} }
if (day == 1) { if (day == 1) {
addExercise('squat', 'Back Squat', ExerciseType.squat, true); addExercise('legs', 'squat', 'Back Squat', ExerciseType.squat, true);
addExercise('pullup', 'Weighted Pull-up', ExerciseType.pullup, false); addExercise(
'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, false);
} else if (day == 2) { } else if (day == 2) {
addExercise('dip', 'Weighted Dip', ExerciseType.dip, true); addExercise('push', 'dip', 'Weighted Dip', ExerciseType.dip, true);
addExercise('squat', 'Back Squat', ExerciseType.squat, false); addExercise('legs', 'squat', 'Back Squat', ExerciseType.squat, false);
} else if (day == 3) { } else if (day == 3) {
addExercise('pullup', 'Weighted Pull-up', ExerciseType.pullup, true); addExercise(
addExercise('dip', 'Weighted Dip', ExerciseType.dip, false); 'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, true);
addExercise('push', 'dip', 'Weighted Dip', ExerciseType.dip, false);
} }
return exercises; return exercises;
@ -152,11 +191,11 @@ class _HubScreenState extends ConsumerState<HubScreen> {
if (workout == null) { if (workout == null) {
final exercises = _generateExercises( final exercises = _generateExercises(
week: targetWeek, week: targetWeek,
day: targetDay, day: targetDay,
trainingMaxes: trainingMaxes, trainingMaxes: trainingMaxes,
bodyweight: user.currentBodyweight, bodyweight: user.currentBodyweight,
); user: user);
final userId = user.serverId ?? user.id.toString(); final userId = user.serverId ?? user.id.toString();
@ -231,16 +270,9 @@ class _HubScreenState extends ConsumerState<HubScreen> {
child: Image.asset( child: Image.asset(
bgItem.assetPath, bgItem.assetPath,
fit: BoxFit.cover, fit: BoxFit.cover,
// Key hinzufügen, damit Flutter einen sanften Übergang animieren kann (optional)
key: ValueKey(bgItem.assetPath), key: ValueKey(bgItem.assetPath),
), ),
), ),
// Positioned.fill(
// child: Image.asset(
// AssetPaths.bgStreetParkDay,
// fit: BoxFit.cover,
// ),
// ),
Positioned.fill( Positioned.fill(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -248,8 +280,10 @@ class _HubScreenState extends ConsumerState<HubScreen> {
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: [
Colors.black.withOpacity(0.6), // Colors.black.withOpacity(0.6),
Colors.black.withOpacity(0.85), Colors.black.withValues(alpha: 0.6),
// Colors.black.withOpacity(0.85),
Colors.black.withValues(alpha: 0.85),
], ],
), ),
), ),
@ -355,7 +389,8 @@ class _HubScreenState extends ConsumerState<HubScreen> {
top: Radius.circular(24)), top: Radius.circular(24)),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.2), // color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, -5), offset: const Offset(0, -5),
), ),
@ -400,6 +435,10 @@ class _HubScreenState extends ConsumerState<HubScreen> {
} }
} }
// extension on Object {
// operator [](String other) {}
// }
class _StatBox extends StatelessWidget { class _StatBox extends StatelessWidget {
final String label; final String label;
final String value; final String value;
@ -417,7 +456,8 @@ class _StatBox extends StatelessWidget {
color: AppTheme.surfaceColor, color: AppTheme.surfaceColor,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.3), // color: AppTheme.primaryColor.withOpacity(0.3),
color: AppTheme.primaryColor.withValues(alpha: 0.3),
), ),
), ),
child: Column( child: Column(

View file

@ -16,19 +16,19 @@ class QuestBoardWidget extends ConsumerWidget {
stream: questRepo.watchQuests(), stream: questRepo.watchQuests(),
builder: (context, snapshot) { builder: (context, snapshot) {
final allQuests = snapshot.data ?? []; final allQuests = snapshot.data ?? [];
// Nur aktive Dailies anzeigen, max 3 final activeQuests = allQuests
final dailies = allQuests .where(
.where((q) => q.type == 'daily' && !q.isClaimed) (q) => !q.isClaimed && (q.type == 'daily' || q.type == 'story'))
.take(3) .take(3)
.toList(); .toList();
if (dailies.isEmpty) return const SizedBox.shrink(); // Ausblenden wenn leer if (activeQuests.isEmpty) return const SizedBox.shrink();
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.surfaceColor, // Oder ein "Holz" Texture für RPG Look color: AppTheme.surfaceColor,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white10), border: Border.all(color: Colors.white10),
), ),
@ -41,19 +41,19 @@ class QuestBoardWidget extends ConsumerWidget {
Text( Text(
'DAILY BOUNTIES', 'DAILY BOUNTIES',
style: Theme.of(context).textTheme.labelSmall?.copyWith( style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: AppTheme.secondaryColor, color: AppTheme.secondaryColor,
letterSpacing: 1.5, letterSpacing: 1.5,
fontWeight: FontWeight.bold fontWeight: FontWeight.bold),
),
), ),
GestureDetector( GestureDetector(
onTap: () => context.go('/quests'), onTap: () => context.go('/quests'),
child: const Text('VIEW ALL >', style: TextStyle(fontSize: 10, color: Colors.grey)), child: const Text('VIEW ALL >',
style: TextStyle(fontSize: 10, color: Colors.grey)),
), ),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
...dailies.map((q) => _MiniQuestRow(quest: q)).toList(), ...activeQuests.map((q) => _MiniQuestRow(quest: q)).toList(),
], ],
), ),
); );

View file

@ -31,6 +31,8 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
final inventorySettings = final inventorySettings =
onboardingData['inventory_settings'] as Map<String, dynamic>; onboardingData['inventory_settings'] as Map<String, dynamic>;
final exerciseVariants =
onboardingData['exercise_variants'] as Map<String, dynamic>?;
var user = await userRepo.getLocalUser(); var user = await userRepo.getLocalUser();
if (user == null) { if (user == null) {
@ -39,6 +41,7 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
password: onboardingData['password'] ?? '', password: onboardingData['password'] ?? '',
bodyweight: onboardingData['bodyweight'] ?? 80.0, bodyweight: onboardingData['bodyweight'] ?? 80.0,
inventorySettings: inventorySettings, inventorySettings: inventorySettings,
exerciseVariants: exerciseVariants,
); );
} else { } else {
user = user.copyWith( user = user.copyWith(

View file

@ -17,12 +17,17 @@ class StrengthTestScreen extends ConsumerStatefulWidget {
class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> { class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _squatWeightController = TextEditingController(text: '100'); final _squatWeightController = TextEditingController(text: '60');
final _squatRepsController = TextEditingController(text: '5'); final _squatRepsController = TextEditingController(text: '5');
final _pullupWeightController = TextEditingController(text: '0');
final _pullupRepsController = TextEditingController(text: '8'); bool _canDoPullup = true;
final _pullWeightController = TextEditingController(text: '0');
final _pullRepsController = TextEditingController(text: '5');
bool _canDoDip = true;
final _dipWeightController = TextEditingController(text: '0'); final _dipWeightController = TextEditingController(text: '0');
final _dipRepsController = TextEditingController(text: '10'); final _benchWeightController = TextEditingController(text: '40');
final _pushRepsController = TextEditingController(text: '5');
Map<String, double> _calculated1RMs = {}; Map<String, double> _calculated1RMs = {};
Map<String, double> _calculatedTMs = {}; Map<String, double> _calculatedTMs = {};
@ -37,10 +42,10 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
void dispose() { void dispose() {
_squatWeightController.dispose(); _squatWeightController.dispose();
_squatRepsController.dispose(); _squatRepsController.dispose();
_pullupWeightController.dispose(); _pullWeightController.dispose();
_pullupRepsController.dispose(); _pullRepsController.dispose();
_dipWeightController.dispose(); _dipWeightController.dispose();
_dipRepsController.dispose(); _pushRepsController.dispose();
super.dispose(); super.dispose();
} }
@ -52,28 +57,40 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
final squat1RM = WendlerCalculator.calculate1RM(squatWeight, squatReps); final squat1RM = WendlerCalculator.calculate1RM(squatWeight, squatReps);
final squatTM = WendlerCalculator.calculateTrainingMax(squat1RM); final squatTM = WendlerCalculator.calculateTrainingMax(squat1RM);
final pullupAdditional = double.tryParse(_pullupWeightController.text) ?? 0; double pull1RM = 0.0;
final pullupReps = int.tryParse(_pullupRepsController.text) ?? 1; if (_canDoPullup) {
final pullupTotal = bodyweight + pullupAdditional; final added = double.tryParse(_pullWeightController.text) ?? 0;
final pullup1RM = WendlerCalculator.calculate1RM(pullupTotal, pullupReps); final reps = int.tryParse(_pullRepsController.text) ?? 1;
final pullupTM = WendlerCalculator.calculateTrainingMax(pullup1RM); pull1RM = WendlerCalculator.calculate1RM(bodyweight + added, reps);
} else {
final weight = double.tryParse(_pullWeightController.text) ?? 0;
final reps = int.tryParse(_pullRepsController.text) ?? 1;
pull1RM = WendlerCalculator.calculate1RM(weight, reps);
}
final pullTM = WendlerCalculator.calculateTrainingMax(pull1RM);
final dipAdditional = double.tryParse(_dipWeightController.text) ?? 0; double push1RM = 0.0;
final dipReps = int.tryParse(_dipRepsController.text) ?? 1; if (_canDoDip) {
final dipTotal = bodyweight + dipAdditional; final added = double.tryParse(_dipWeightController.text) ?? 0;
final dip1RM = WendlerCalculator.calculate1RM(dipTotal, dipReps); final reps = int.tryParse(_pushRepsController.text) ?? 1;
final dipTM = WendlerCalculator.calculateTrainingMax(dip1RM); push1RM = WendlerCalculator.calculate1RM(bodyweight + added, reps);
} else {
final weight = double.tryParse(_benchWeightController.text) ?? 0;
final reps = int.tryParse(_pushRepsController.text) ?? 1;
push1RM = WendlerCalculator.calculate1RM(weight, reps);
}
final pushTM = WendlerCalculator.calculateTrainingMax(push1RM);
setState(() { setState(() {
_calculated1RMs = { _calculated1RMs = {
'squat': squat1RM, 'squat': squat1RM,
'pullup': pullup1RM, 'pullup': pull1RM,
'dip': dip1RM, 'dip': push1RM,
}; };
_calculatedTMs = { _calculatedTMs = {
'squat': squatTM, 'squat': squatTM,
'pullup': pullupTM, 'pullup': pullTM,
'dip': dipTM, 'dip': pushTM,
}; };
}); });
} }
@ -81,9 +98,15 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
void _handleContinue() { void _handleContinue() {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
final variants = <String, String>{
'pull': _canDoPullup ? 'pullup' : 'row',
'push': _canDoDip ? 'dip' : 'bench',
};
ref.read(onboardingDataProvider.notifier).update((state) => { ref.read(onboardingDataProvider.notifier).update((state) => {
...state, ...state,
'training_maxes': _calculatedTMs, 'training_maxes': _calculatedTMs,
'exercise_variants': variants,
}); });
context.go('/onboarding/inventory'); context.go('/onboarding/inventory');
@ -91,8 +114,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bodyweight = ref.watch(onboardingDataProvider)['bodyweight'] ?? 80.0;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Strength Test'), title: const Text('Strength Test'),
@ -121,47 +142,70 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'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.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8),
Text(
'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),
_ExerciseCard( _ExerciseCard(
title: 'Leg Strength',
exerciseName: 'Back Squat', exerciseName: 'Back Squat',
icon: Icons.accessibility_new, icon: Icons.accessibility_new,
weightController: _squatWeightController, weightController: _squatWeightController,
repsController: _squatRepsController, repsController: _squatRepsController,
isBodyweight: false, isBodyweight: false,
calculated1RM: _calculated1RMs['squat'] ?? 0,
calculatedTM: _calculatedTMs['squat'] ?? 0,
onChanged: _calculateAll, onChanged: _calculateAll,
result1RM: _calculated1RMs['squat'] ?? 0,
resultTM: _calculatedTMs['squat'] ?? 0,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_ExerciseCard( _AdaptiveExerciseCard(
exerciseName: 'Weighted Pull-up', slotTitle: 'Pull Strength',
primaryName: 'Weighted Pull-up',
secondaryName: 'Pendlay Row',
icon: Icons.north, icon: Icons.north,
weightController: _pullupWeightController, isCapable: _canDoPullup,
repsController: _pullupRepsController, onToggleCapability: (val) {
isBodyweight: true, setState(() {
bodyweight: bodyweight, _canDoPullup = val;
calculated1RM: _calculated1RMs['pullup'] ?? 0, _pullWeightController.text = '0';
calculatedTM: _calculatedTMs['pullup'] ?? 0, _pullRepsController.text = '5';
_calculateAll();
});
},
weightController: _pullWeightController,
repsController: _pullRepsController,
weightLabel:
_canDoPullup ? 'Add. Weight (kg)' : 'Row Weight (kg)',
repsLabel: _canDoPullup ? 'Reps' : '5RM Reps (usually 5)',
showResults: _canDoPullup || true,
result1RM: _calculated1RMs['pullup'] ?? 0,
resultTM: _calculatedTMs['pullup'] ?? 0,
onChanged: _calculateAll, onChanged: _calculateAll,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_ExerciseCard( _AdaptiveExerciseCard(
exerciseName: 'Weighted Dip', slotTitle: 'Push Strength',
primaryName: 'Weighted Dip',
secondaryName: 'Bench Press',
icon: Icons.south, icon: Icons.south,
weightController: _dipWeightController, isCapable: _canDoDip,
repsController: _dipRepsController, onToggleCapability: (val) {
isBodyweight: true, setState(() {
bodyweight: bodyweight, _canDoDip = val;
calculated1RM: _calculated1RMs['dip'] ?? 0, _dipWeightController.text = '0';
calculatedTM: _calculatedTMs['dip'] ?? 0, _pushRepsController.text = '5';
_calculateAll();
});
},
weightController:
_canDoDip ? _dipWeightController : _benchWeightController,
repsController: _pushRepsController,
weightLabel: _canDoDip ? 'Add. Weight (kg)' : 'Weight (kg)',
repsLabel: 'Reps',
showWeightInput: true,
showResults: true,
result1RM: _calculated1RMs['dip'] ?? 0,
resultTM: _calculatedTMs['dip'] ?? 0,
onChanged: _calculateAll, onChanged: _calculateAll,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
@ -171,19 +215,16 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
color: AppTheme.primaryColor.withOpacity(0.1), color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.3), color: AppTheme.primaryColor.withOpacity(0.3)),
),
), ),
child: Row( child: Row(
children: [ children: [
const Icon( const Icon(Icons.info_outline,
Icons.info_outline, color: AppTheme.primaryColor),
color: AppTheme.primaryColor,
),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
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 (90% of 1RM). For bodyweight exercises, we adjust the strategy.',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodySmall .bodySmall
@ -208,25 +249,25 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
} }
class _ExerciseCard extends StatelessWidget { class _ExerciseCard extends StatelessWidget {
final String title;
final String exerciseName; final String exerciseName;
final IconData icon; final IconData icon;
final TextEditingController weightController; final TextEditingController weightController;
final TextEditingController repsController; final TextEditingController repsController;
final bool isBodyweight; final bool isBodyweight;
final double bodyweight; final double result1RM;
final double calculated1RM; final double resultTM;
final double calculatedTM;
final VoidCallback onChanged; final VoidCallback onChanged;
const _ExerciseCard({ const _ExerciseCard({
required this.title,
required this.exerciseName, required this.exerciseName,
required this.icon, required this.icon,
required this.weightController, required this.weightController,
required this.repsController, required this.repsController,
this.isBodyweight = false, required this.isBodyweight,
this.bodyweight = 0, required this.result1RM,
required this.calculated1RM, required this.resultTM,
required this.calculatedTM,
required this.onChanged, required this.onChanged,
}); });
@ -238,27 +279,27 @@ class _ExerciseCard extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header Text(title.toUpperCase(),
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Row( Row(
children: [ children: [
Container( Container(
width: 40, padding: const EdgeInsets.all(8),
height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.2), color: AppTheme.primaryColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8)),
),
child: Icon(icon, color: AppTheme.primaryColor), child: Icon(icon, color: AppTheme.primaryColor),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(exerciseName,
exerciseName, style: Theme.of(context).textTheme.titleLarge),
style: Theme.of(context).textTheme.titleLarge,
),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -268,21 +309,14 @@ class _ExerciseCard extends StatelessWidget {
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.allow( FilteringTextInputFormatter.allow(
RegExp(r'^\d+\.?\d{0,2}')), RegExp(r'^\d+\.?\d{0,2}'))
], ],
decoration: InputDecoration( decoration: InputDecoration(
labelText: isBodyweight labelText:
? 'Additional Weight (kg)' isBodyweight ? 'Add. Weight (kg)' : 'Weight (kg)',
: 'Weight (kg)', isDense: true),
isDense: true,
),
onChanged: (_) => onChanged(), onChanged: (_) => onChanged(),
validator: (value) { validator: (v) => v!.isEmpty ? 'Required' : null,
if (value == null || value.isEmpty) {
return 'Required';
}
return null;
},
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -290,56 +324,17 @@ class _ExerciseCard extends StatelessWidget {
child: TextFormField( child: TextFormField(
controller: repsController, controller: repsController,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: [ inputFormatters: [FilteringTextInputFormatter.digitsOnly],
FilteringTextInputFormatter.digitsOnly, decoration:
], const InputDecoration(labelText: 'Reps', isDense: true),
decoration: const InputDecoration(
labelText: 'Reps',
isDense: true,
),
onChanged: (_) => onChanged(), onChanged: (_) => onChanged(),
validator: (value) { validator: (v) => v!.isEmpty ? 'Required' : null,
if (value == null || value.isEmpty) {
return 'Required';
}
final reps = int.tryParse(value);
if (reps == null || reps < 1 || reps > 20) {
return '1-20';
}
return null;
},
), ),
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_ResultBox(rm: result1RM, tm: resultTM),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.surfaceColor,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
if (isBodyweight)
_ResultRow(
label: 'Total Weight',
value:
'${(bodyweight + (double.tryParse(weightController.text) ?? 0)).toStringAsFixed(1)} kg',
),
_ResultRow(
label: 'Estimated 1RM',
value: '${calculated1RM.toStringAsFixed(1)} kg',
),
_ResultRow(
label: 'Training Max (90%)',
value: '${calculatedTM.toStringAsFixed(1)} kg',
highlight: true,
),
],
),
),
], ],
), ),
), ),
@ -347,36 +342,176 @@ class _ExerciseCard extends StatelessWidget {
} }
} }
class _ResultRow extends StatelessWidget { class _AdaptiveExerciseCard extends StatelessWidget {
final String label; final String slotTitle;
final String value; final String primaryName;
final bool highlight; final String secondaryName;
final IconData icon;
final bool isCapable;
final ValueChanged<bool> onToggleCapability;
final TextEditingController weightController;
final TextEditingController repsController;
final String weightLabel;
final String repsLabel;
final bool showWeightInput;
final bool showResults;
final double result1RM;
final double resultTM;
final VoidCallback onChanged;
const _ResultRow({ const _AdaptiveExerciseCard({
required this.label, required this.slotTitle,
required this.value, required this.primaryName,
this.highlight = false, required this.secondaryName,
required this.icon,
required this.isCapable,
required this.onToggleCapability,
required this.weightController,
required this.repsController,
required this.weightLabel,
required this.repsLabel,
this.showWeightInput = true,
this.showResults = true,
required this.result1RM,
required this.resultTM,
required this.onChanged,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Card(
padding: const EdgeInsets.symmetric(vertical: 4), child: Padding(
child: Row( padding: const EdgeInsets.all(16),
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(slotTitle.toUpperCase(),
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold)),
Row(
children: [
Text('Can do 1 rep?',
style: TextStyle(
fontSize: 12,
color: isCapable
? AppTheme.successColor
: Colors.grey)),
Switch(
value: isCapable,
activeColor: AppTheme.successColor,
onChanged: onToggleCapability,
),
],
),
],
),
const SizedBox(height: 4),
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(8)),
child: Icon(icon, color: AppTheme.primaryColor),
),
const SizedBox(width: 12),
Text(isCapable ? primaryName : secondaryName,
style: Theme.of(context).textTheme.titleLarge),
],
),
if (!isCapable) ...[
const SizedBox(height: 8),
Text(
'Adjusted Strategy: ${isCapable ? "Wendler 5/3/1" : "Linear Progression (3x5)"}',
style: const TextStyle(
color: AppTheme.secondaryColor,
fontSize: 12,
fontStyle: FontStyle.italic),
),
],
const SizedBox(height: 16),
Row(
children: [
if (showWeightInput)
Expanded(
flex: 2,
child: TextFormField(
controller: weightController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d+\.?\d{0,2}'))
],
decoration: InputDecoration(
labelText: weightLabel, isDense: true),
onChanged: (_) => onChanged(),
validator: (v) => v!.isEmpty ? 'Required' : null,
),
)
else
const Spacer(flex: 2),
if (showWeightInput) const SizedBox(width: 12),
Expanded(
child: TextFormField(
controller: repsController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration:
InputDecoration(labelText: repsLabel, isDense: true),
onChanged: (_) => onChanged(),
validator: (v) => v!.isEmpty ? 'Required' : null,
),
),
],
),
if (showResults) ...[
const SizedBox(height: 16),
_ResultBox(rm: result1RM, tm: resultTM),
],
],
),
),
);
}
}
class _ResultBox extends StatelessWidget {
final double rm;
final double tm;
const _ResultBox({required this.rm, required this.tm});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.surfaceColor, borderRadius: BorderRadius.circular(8)),
child: Column(
children: [ children: [
Text( Row(
label, mainAxisAlignment: MainAxisAlignment.spaceBetween,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( children: [
fontWeight: highlight ? FontWeight.bold : null, const Text('Est. 1RM'),
), Text('${rm.toStringAsFixed(1)} kg',
style: Theme.of(context).textTheme.bodyLarge),
],
), ),
Text( const SizedBox(height: 4),
value, Row(
style: Theme.of(context).textTheme.bodyLarge?.copyWith( mainAxisAlignment: MainAxisAlignment.spaceBetween,
color: highlight ? AppTheme.primaryColor : null, children: [
fontWeight: highlight ? FontWeight.bold : null, const Text('Training Max (90%)'),
), Text('${tm.toStringAsFixed(1)} kg',
style: const TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold)),
],
), ),
], ],
), ),

View file

@ -1,9 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:slrpg_app/src/shared/data/local/tables.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../shared/data/local/app_database.dart'; // Drift Models import '../../../../shared/data/local/app_database.dart';
import '../../../../shared/data/repositories/cycle_repository.dart'; import '../../../../shared/data/repositories/cycle_repository.dart';
import '../../../../shared/data/repositories/user_repository.dart'; import '../../../../shared/data/repositories/user_repository.dart';
import '../../../../shared/data/repositories/workout_repository.dart'; import '../../../../shared/data/repositories/workout_repository.dart';
@ -22,7 +23,7 @@ class StatsScreen extends ConsumerStatefulWidget {
class _StatsScreenState extends ConsumerState<StatsScreen> { class _StatsScreenState extends ConsumerState<StatsScreen> {
bool _isLoading = false; bool _isLoading = false;
String _selectedExercise = 'squat'; // squat, pullup, dip String _selectedExercise = 'squat';
String _selectedRange = '3m'; // 1m, 3m, 1y, all String _selectedRange = '3m'; // 1m, 3m, 1y, all
List<StatsDataPoint> _chartData = []; List<StatsDataPoint> _chartData = [];
bool _isChartLoading = true; bool _isChartLoading = true;
@ -175,6 +176,7 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cycleRepo = ref.watch(cycleRepositoryProvider); final cycleRepo = ref.watch(cycleRepositoryProvider);
final userRepo = ref.watch(userRepositoryProvider);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -184,14 +186,38 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
onPressed: () => context.go('/hub'), onPressed: () => context.go('/hub'),
), ),
), ),
body: FutureBuilder<CycleCollection?>( body: FutureBuilder<List<dynamic>>(
future: cycleRepo.getCurrentCycle(), future: Future.wait([
cycleRepo.getCurrentCycle(),
userRepo.getLocalUser(),
]),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
final currentCycle = snapshot.data; final currentCycle = snapshot.data?[0] as CycleCollection?;
final user = snapshot.data?[1] as UserCollection;
final variants = user.exerciseVariants ?? {};
final pullVariant = variants['pull'] ?? 'pullup';
final pushVariant = variants['push'] ?? 'dip';
String getLabel(String id) {
switch (id) {
case 'squat':
return 'Squat';
case 'pullup':
return 'Pull-up';
case 'row':
return 'Row';
case 'dip':
return 'Dip';
case 'bench':
return 'Bench';
default:
return id;
}
}
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -201,6 +227,7 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
if (currentCycle != null) ...[ if (currentCycle != null) ...[
_CurrentCycleCard( _CurrentCycleCard(
cycle: currentCycle, cycle: currentCycle,
user: user,
onFinish: _isLoading onFinish: _isLoading
? null ? null
: () => _handleFinishCycle(currentCycle), : () => _handleFinishCycle(currentCycle),
@ -226,15 +253,17 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
_FilterChip( _FilterChip(
label: 'Pull-up', label: getLabel(pullVariant),
isSelected: _selectedExercise == 'pullup', isSelected: _selectedExercise == pullVariant,
onTap: () => _onFilterChanged('pullup', _selectedRange), onTap: () =>
_onFilterChanged(pullVariant, _selectedRange),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
_FilterChip( _FilterChip(
label: 'Dip', label: getLabel(pushVariant),
isSelected: _selectedExercise == 'dip', isSelected: _selectedExercise == pushVariant,
onTap: () => _onFilterChanged('dip', _selectedRange), onTap: () =>
_onFilterChanged(pushVariant, _selectedRange),
), ),
], ],
), ),
@ -256,14 +285,35 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
class _CurrentCycleCard extends StatelessWidget { class _CurrentCycleCard extends StatelessWidget {
final CycleCollection cycle; final CycleCollection cycle;
final UserCollection user;
final VoidCallback? onFinish; final VoidCallback? onFinish;
const _CurrentCycleCard({required this.cycle, required this.onFinish}); const _CurrentCycleCard(
{required this.cycle, required this.user, required this.onFinish});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Drift: Direct access
final tms = cycle.trainingMaxes; final tms = cycle.trainingMaxes;
final variants = user.exerciseVariants ?? {};
final pullVariant = variants['pull'] ?? 'pullup';
final pushVariant = variants['push'] ?? 'dip';
String getLabel(String id) {
switch (id) {
case 'squat':
return 'Squat';
case 'pullup':
return 'Pull-up';
case 'row':
return 'Row';
case 'dip':
return 'Dip';
case 'bench':
return 'Bench';
default:
return id;
}
}
return Card( return Card(
child: Padding( child: Padding(
@ -303,8 +353,9 @@ class _CurrentCycleCard extends StatelessWidget {
style: Theme.of(context).textTheme.labelLarge), style: Theme.of(context).textTheme.labelLarge),
const SizedBox(height: 16), const SizedBox(height: 16),
_StatRow(label: 'Squat', value: '${tms['squat']} kg'), _StatRow(label: 'Squat', value: '${tms['squat']} kg'),
_StatRow(label: 'Pull-up', value: '${tms['pullup']} kg'), _StatRow(
_StatRow(label: 'Dip', value: '${tms['dip']} kg'), label: getLabel(pullVariant), value: '${tms['pullup']} kg'),
_StatRow(label: getLabel(pushVariant), value: '${tms['dip']} kg'),
const SizedBox(height: 32), const SizedBox(height: 32),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,

View file

@ -5,6 +5,7 @@ import 'dart:async';
import '../../../../core/constants/asset_paths.dart'; import '../../../../core/constants/asset_paths.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../shared/data/local/app_database.dart';
import '../../../../shared/domain/entities/exercise.dart'; import '../../../../shared/domain/entities/exercise.dart';
import '../../../../shared/domain/entities/workout_set.dart'; import '../../../../shared/domain/entities/workout_set.dart';
import '../../../../shared/domain/logic/wendler_calculator.dart'; import '../../../../shared/domain/logic/wendler_calculator.dart';
@ -62,17 +63,52 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
case 'squat': case 'squat':
return AssetPaths.enemyIronGolem; return AssetPaths.enemyIronGolem;
case 'pullup': case 'pullup':
case 'row':
return AssetPaths.enemyGravityDemon; return AssetPaths.enemyGravityDemon;
case 'dip': case 'dip':
case 'bench':
return AssetPaths.enemyPressurePhantom; return AssetPaths.enemyPressurePhantom;
default: default:
return AssetPaths.enemyIronGolem; return AssetPaths.enemyIronGolem;
} }
} }
List<Map<String, dynamic>> _getExerciseConfig(int day) { List<Map<String, dynamic>> _getExerciseConfig(int day, UserCollection user) {
final variants = user.exerciseVariants ?? {};
Map<String, dynamic> getVariant(String slot, String defaultId,
String defaultName, ExerciseType defaultType) {
final variant = variants[slot];
if (slot == 'pull') {
if (variant == 'row') {
return {'id': 'row', 'name': 'Pendlay Row', 'type': ExerciseType.row};
}
return {
'id': 'pullup',
'name': 'Weighted Pull-up',
'type': ExerciseType.pullup
};
}
if (slot == 'push') {
if (variant == 'bench') {
return {
'id': 'bench',
'name': 'Bench Press',
'type': ExerciseType.bench
};
}
return {'id': 'dip', 'name': 'Weighted Dip', 'type': ExerciseType.dip};
}
return {'id': defaultId, 'name': defaultName, 'type': defaultType};
}
switch (day) { switch (day) {
case 1: case 1:
final pull = getVariant(
'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup);
return [ return [
{ {
'id': 'squat', 'id': 'squat',
@ -80,21 +116,13 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
'type': ExerciseType.squat, 'type': ExerciseType.squat,
'isMain': true 'isMain': true
}, },
{ {...pull, 'isMain': false},
'id': 'pullup',
'name': 'Weighted Pull-up',
'type': ExerciseType.pullup,
'isMain': false
},
]; ];
case 2: case 2:
final push =
getVariant('push', 'dip', 'Weighted Dip', ExerciseType.dip);
return [ return [
{ {...push, 'isMain': true},
'id': 'dip',
'name': 'Weighted Dip',
'type': ExerciseType.dip,
'isMain': true
},
{ {
'id': 'squat', 'id': 'squat',
'name': 'Back Squat', 'name': 'Back Squat',
@ -103,19 +131,13 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
}, },
]; ];
case 3: case 3:
final pull = getVariant(
'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup);
final push =
getVariant('push', 'dip', 'Weighted Dip', ExerciseType.dip);
return [ return [
{ {...pull, 'isMain': true},
'id': 'pullup', {...push, 'isMain': false},
'name': 'Weighted Pull-up',
'type': ExerciseType.pullup,
'isMain': true
},
{
'id': 'dip',
'name': 'Weighted Dip',
'type': ExerciseType.dip,
'isMain': false
},
]; ];
default: default:
return []; return [];
@ -135,7 +157,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
} }
final exercises = <Exercise>[]; final exercises = <Exercise>[];
final exerciseConfigs = _getExerciseConfig(widget.day); final exerciseConfigs = _getExerciseConfig(widget.day, user);
for (final config in exerciseConfigs) { for (final config in exerciseConfigs) {
final id = config['id'] as String; final id = config['id'] as String;
@ -143,7 +165,11 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
final type = config['type'] as ExerciseType; final type = config['type'] as ExerciseType;
final isMain = config['isMain'] as bool; final isMain = config['isMain'] as bool;
final tm = trainingMaxesMap[id] ?? 0.0; String tmKey = id;
if (id == 'bench') tmKey = 'dip';
if (id == 'row') tmKey = 'pullup';
final tm = trainingMaxesMap[tmKey] ?? 0.0;
List<WorkoutSet> sets = []; List<WorkoutSet> sets = [];
if (isMain) { if (isMain) {
@ -454,7 +480,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
.take(_currentSetIndex) .take(_currentSetIndex)
.fold<int>(0, (sum, set) => sum + set.repsActual); .fold<int>(0, (sum, set) => sum + set.repsActual);
final isBodyweight = currentExercise.exerciseId != 'squat'; final isTwoSided = currentExercise.exerciseId == 'squat' ||
currentExercise.exerciseId == 'row' ||
currentExercise.exerciseId == 'bench';
final isBodyweight = !isTwoSided;
final barWeight = isBodyweight final barWeight = isBodyweight
? currentExercise.bodyweightAtSession ? currentExercise.bodyweightAtSession
: (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0; : (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0;
@ -480,7 +509,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
barWeight: barWeight, barWeight: barWeight,
availablePlates: platesList, availablePlates: platesList,
availableBands: availableBands, availableBands: availableBands,
isTwoSided: !isBodyweight, isTwoSided: isTwoSided,
); );
return Scaffold( return Scaffold(
@ -764,7 +793,9 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
else else
PlateVisualizer( PlateVisualizer(
plateConfiguration: plateResult.plateConfiguration, plateConfiguration: plateResult.plateConfiguration,
isTwoSided: currentExercise.exerciseId == 'squat', isTwoSided: currentExercise.exerciseId == 'squat' ||
currentExercise.exerciseId == 'row' ||
currentExercise.exerciseId == 'bench',
exerciseName: currentExercise.exerciseName, exerciseName: currentExercise.exerciseName,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),

View file

@ -13,17 +13,17 @@ class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection()); AppDatabase() : super(_openConnection());
@override @override
int get schemaVersion => 2; int get schemaVersion => 3;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
onCreate: (Migrator m) async {
await m.createAll();
},
onUpgrade: (Migrator m, int from, int to) async { onUpgrade: (Migrator m, int from, int to) async {
if (from < 2) { if (from < 2) {
await m.createTable(quests); await m.createTable(quests);
} }
if (from < 3) {
await m.addColumn(users, users.exerciseVariants);
}
}, },
); );
} }

View file

@ -55,6 +55,13 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserCollection> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: const Constant(70.0)); defaultValue: const Constant(70.0));
@override @override
late final GeneratedColumnWithTypeConverter<Map<String, dynamic>?, String>
exerciseVariants = GeneratedColumn<String>(
'exercise_variants', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false)
.withConverter<Map<String, dynamic>?>(
$UsersTable.$converterexerciseVariantsn);
@override
late final GeneratedColumnWithTypeConverter<Map<String, dynamic>?, String> late final GeneratedColumnWithTypeConverter<Map<String, dynamic>?, String>
inventorySettings = GeneratedColumn<String>( inventorySettings = GeneratedColumn<String>(
'inventory_settings', aliasedName, true, 'inventory_settings', aliasedName, true,
@ -107,6 +114,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserCollection> {
xp, xp,
level, level,
currentBodyweight, currentBodyweight,
exerciseVariants,
inventorySettings, inventorySettings,
avatarConfig, avatarConfig,
lastSyncAt, lastSyncAt,
@ -187,6 +195,9 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserCollection> {
.read(DriftSqlType.int, data['${effectivePrefix}level'])!, .read(DriftSqlType.int, data['${effectivePrefix}level'])!,
currentBodyweight: attachedDatabase.typeMapping.read( currentBodyweight: attachedDatabase.typeMapping.read(
DriftSqlType.double, data['${effectivePrefix}current_bodyweight'])!, DriftSqlType.double, data['${effectivePrefix}current_bodyweight'])!,
exerciseVariants: $UsersTable.$converterexerciseVariantsn.fromSql(
attachedDatabase.typeMapping.read(DriftSqlType.string,
data['${effectivePrefix}exercise_variants'])),
inventorySettings: $UsersTable.$converterinventorySettingsn.fromSql( inventorySettings: $UsersTable.$converterinventorySettingsn.fromSql(
attachedDatabase.typeMapping.read(DriftSqlType.string, attachedDatabase.typeMapping.read(DriftSqlType.string,
data['${effectivePrefix}inventory_settings'])), data['${effectivePrefix}inventory_settings'])),
@ -209,6 +220,11 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserCollection> {
return $UsersTable(attachedDatabase, alias); return $UsersTable(attachedDatabase, alias);
} }
static TypeConverter<Map<String, dynamic>, String>
$converterexerciseVariants = const MapConverter();
static TypeConverter<Map<String, dynamic>?, String?>
$converterexerciseVariantsn =
NullAwareTypeConverter.wrap($converterexerciseVariants);
static TypeConverter<Map<String, dynamic>, String> static TypeConverter<Map<String, dynamic>, String>
$converterinventorySettings = const MapConverter(); $converterinventorySettings = const MapConverter();
static TypeConverter<Map<String, dynamic>?, String?> static TypeConverter<Map<String, dynamic>?, String?>
@ -227,6 +243,7 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
final int xp; final int xp;
final int level; final int level;
final double currentBodyweight; final double currentBodyweight;
final Map<String, dynamic>? exerciseVariants;
final Map<String, dynamic>? inventorySettings; final Map<String, dynamic>? inventorySettings;
final Map<String, dynamic>? avatarConfig; final Map<String, dynamic>? avatarConfig;
final DateTime? lastSyncAt; final DateTime? lastSyncAt;
@ -240,6 +257,7 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
required this.xp, required this.xp,
required this.level, required this.level,
required this.currentBodyweight, required this.currentBodyweight,
this.exerciseVariants,
this.inventorySettings, this.inventorySettings,
this.avatarConfig, this.avatarConfig,
this.lastSyncAt, this.lastSyncAt,
@ -257,6 +275,10 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
map['xp'] = Variable<int>(xp); map['xp'] = Variable<int>(xp);
map['level'] = Variable<int>(level); map['level'] = Variable<int>(level);
map['current_bodyweight'] = Variable<double>(currentBodyweight); map['current_bodyweight'] = Variable<double>(currentBodyweight);
if (!nullToAbsent || exerciseVariants != null) {
map['exercise_variants'] = Variable<String>(
$UsersTable.$converterexerciseVariantsn.toSql(exerciseVariants));
}
if (!nullToAbsent || inventorySettings != null) { if (!nullToAbsent || inventorySettings != null) {
map['inventory_settings'] = Variable<String>( map['inventory_settings'] = Variable<String>(
$UsersTable.$converterinventorySettingsn.toSql(inventorySettings)); $UsersTable.$converterinventorySettingsn.toSql(inventorySettings));
@ -284,6 +306,9 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
xp: Value(xp), xp: Value(xp),
level: Value(level), level: Value(level),
currentBodyweight: Value(currentBodyweight), currentBodyweight: Value(currentBodyweight),
exerciseVariants: exerciseVariants == null && nullToAbsent
? const Value.absent()
: Value(exerciseVariants),
inventorySettings: inventorySettings == null && nullToAbsent inventorySettings: inventorySettings == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(inventorySettings), : Value(inventorySettings),
@ -309,6 +334,8 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
xp: serializer.fromJson<int>(json['xp']), xp: serializer.fromJson<int>(json['xp']),
level: serializer.fromJson<int>(json['level']), level: serializer.fromJson<int>(json['level']),
currentBodyweight: serializer.fromJson<double>(json['currentBodyweight']), currentBodyweight: serializer.fromJson<double>(json['currentBodyweight']),
exerciseVariants:
serializer.fromJson<Map<String, dynamic>?>(json['exerciseVariants']),
inventorySettings: inventorySettings:
serializer.fromJson<Map<String, dynamic>?>(json['inventorySettings']), serializer.fromJson<Map<String, dynamic>?>(json['inventorySettings']),
avatarConfig: avatarConfig:
@ -329,6 +356,8 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
'xp': serializer.toJson<int>(xp), 'xp': serializer.toJson<int>(xp),
'level': serializer.toJson<int>(level), 'level': serializer.toJson<int>(level),
'currentBodyweight': serializer.toJson<double>(currentBodyweight), 'currentBodyweight': serializer.toJson<double>(currentBodyweight),
'exerciseVariants':
serializer.toJson<Map<String, dynamic>?>(exerciseVariants),
'inventorySettings': 'inventorySettings':
serializer.toJson<Map<String, dynamic>?>(inventorySettings), serializer.toJson<Map<String, dynamic>?>(inventorySettings),
'avatarConfig': serializer.toJson<Map<String, dynamic>?>(avatarConfig), 'avatarConfig': serializer.toJson<Map<String, dynamic>?>(avatarConfig),
@ -346,6 +375,7 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
int? xp, int? xp,
int? level, int? level,
double? currentBodyweight, double? currentBodyweight,
Value<Map<String, dynamic>?> exerciseVariants = const Value.absent(),
Value<Map<String, dynamic>?> inventorySettings = const Value.absent(), Value<Map<String, dynamic>?> inventorySettings = const Value.absent(),
Value<Map<String, dynamic>?> avatarConfig = const Value.absent(), Value<Map<String, dynamic>?> avatarConfig = const Value.absent(),
Value<DateTime?> lastSyncAt = const Value.absent(), Value<DateTime?> lastSyncAt = const Value.absent(),
@ -359,6 +389,9 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
xp: xp ?? this.xp, xp: xp ?? this.xp,
level: level ?? this.level, level: level ?? this.level,
currentBodyweight: currentBodyweight ?? this.currentBodyweight, currentBodyweight: currentBodyweight ?? this.currentBodyweight,
exerciseVariants: exerciseVariants.present
? exerciseVariants.value
: this.exerciseVariants,
inventorySettings: inventorySettings.present inventorySettings: inventorySettings.present
? inventorySettings.value ? inventorySettings.value
: this.inventorySettings, : this.inventorySettings,
@ -379,6 +412,9 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
currentBodyweight: data.currentBodyweight.present currentBodyweight: data.currentBodyweight.present
? data.currentBodyweight.value ? data.currentBodyweight.value
: this.currentBodyweight, : this.currentBodyweight,
exerciseVariants: data.exerciseVariants.present
? data.exerciseVariants.value
: this.exerciseVariants,
inventorySettings: data.inventorySettings.present inventorySettings: data.inventorySettings.present
? data.inventorySettings.value ? data.inventorySettings.value
: this.inventorySettings, : this.inventorySettings,
@ -402,6 +438,7 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
..write('xp: $xp, ') ..write('xp: $xp, ')
..write('level: $level, ') ..write('level: $level, ')
..write('currentBodyweight: $currentBodyweight, ') ..write('currentBodyweight: $currentBodyweight, ')
..write('exerciseVariants: $exerciseVariants, ')
..write('inventorySettings: $inventorySettings, ') ..write('inventorySettings: $inventorySettings, ')
..write('avatarConfig: $avatarConfig, ') ..write('avatarConfig: $avatarConfig, ')
..write('lastSyncAt: $lastSyncAt, ') ..write('lastSyncAt: $lastSyncAt, ')
@ -420,6 +457,7 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
xp, xp,
level, level,
currentBodyweight, currentBodyweight,
exerciseVariants,
inventorySettings, inventorySettings,
avatarConfig, avatarConfig,
lastSyncAt, lastSyncAt,
@ -436,6 +474,7 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
other.xp == this.xp && other.xp == this.xp &&
other.level == this.level && other.level == this.level &&
other.currentBodyweight == this.currentBodyweight && other.currentBodyweight == this.currentBodyweight &&
other.exerciseVariants == this.exerciseVariants &&
other.inventorySettings == this.inventorySettings && other.inventorySettings == this.inventorySettings &&
other.avatarConfig == this.avatarConfig && other.avatarConfig == this.avatarConfig &&
other.lastSyncAt == this.lastSyncAt && other.lastSyncAt == this.lastSyncAt &&
@ -451,6 +490,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
final Value<int> xp; final Value<int> xp;
final Value<int> level; final Value<int> level;
final Value<double> currentBodyweight; final Value<double> currentBodyweight;
final Value<Map<String, dynamic>?> exerciseVariants;
final Value<Map<String, dynamic>?> inventorySettings; final Value<Map<String, dynamic>?> inventorySettings;
final Value<Map<String, dynamic>?> avatarConfig; final Value<Map<String, dynamic>?> avatarConfig;
final Value<DateTime?> lastSyncAt; final Value<DateTime?> lastSyncAt;
@ -464,6 +504,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
this.xp = const Value.absent(), this.xp = const Value.absent(),
this.level = const Value.absent(), this.level = const Value.absent(),
this.currentBodyweight = const Value.absent(), this.currentBodyweight = const Value.absent(),
this.exerciseVariants = const Value.absent(),
this.inventorySettings = const Value.absent(), this.inventorySettings = const Value.absent(),
this.avatarConfig = const Value.absent(), this.avatarConfig = const Value.absent(),
this.lastSyncAt = const Value.absent(), this.lastSyncAt = const Value.absent(),
@ -478,6 +519,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
this.xp = const Value.absent(), this.xp = const Value.absent(),
this.level = const Value.absent(), this.level = const Value.absent(),
this.currentBodyweight = const Value.absent(), this.currentBodyweight = const Value.absent(),
this.exerciseVariants = const Value.absent(),
this.inventorySettings = const Value.absent(), this.inventorySettings = const Value.absent(),
this.avatarConfig = const Value.absent(), this.avatarConfig = const Value.absent(),
this.lastSyncAt = const Value.absent(), this.lastSyncAt = const Value.absent(),
@ -492,6 +534,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
Expression<int>? xp, Expression<int>? xp,
Expression<int>? level, Expression<int>? level,
Expression<double>? currentBodyweight, Expression<double>? currentBodyweight,
Expression<String>? exerciseVariants,
Expression<String>? inventorySettings, Expression<String>? inventorySettings,
Expression<String>? avatarConfig, Expression<String>? avatarConfig,
Expression<DateTime>? lastSyncAt, Expression<DateTime>? lastSyncAt,
@ -506,6 +549,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
if (xp != null) 'xp': xp, if (xp != null) 'xp': xp,
if (level != null) 'level': level, if (level != null) 'level': level,
if (currentBodyweight != null) 'current_bodyweight': currentBodyweight, if (currentBodyweight != null) 'current_bodyweight': currentBodyweight,
if (exerciseVariants != null) 'exercise_variants': exerciseVariants,
if (inventorySettings != null) 'inventory_settings': inventorySettings, if (inventorySettings != null) 'inventory_settings': inventorySettings,
if (avatarConfig != null) 'avatar_config': avatarConfig, if (avatarConfig != null) 'avatar_config': avatarConfig,
if (lastSyncAt != null) 'last_sync_at': lastSyncAt, if (lastSyncAt != null) 'last_sync_at': lastSyncAt,
@ -522,6 +566,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
Value<int>? xp, Value<int>? xp,
Value<int>? level, Value<int>? level,
Value<double>? currentBodyweight, Value<double>? currentBodyweight,
Value<Map<String, dynamic>?>? exerciseVariants,
Value<Map<String, dynamic>?>? inventorySettings, Value<Map<String, dynamic>?>? inventorySettings,
Value<Map<String, dynamic>?>? avatarConfig, Value<Map<String, dynamic>?>? avatarConfig,
Value<DateTime?>? lastSyncAt, Value<DateTime?>? lastSyncAt,
@ -535,6 +580,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
xp: xp ?? this.xp, xp: xp ?? this.xp,
level: level ?? this.level, level: level ?? this.level,
currentBodyweight: currentBodyweight ?? this.currentBodyweight, currentBodyweight: currentBodyweight ?? this.currentBodyweight,
exerciseVariants: exerciseVariants ?? this.exerciseVariants,
inventorySettings: inventorySettings ?? this.inventorySettings, inventorySettings: inventorySettings ?? this.inventorySettings,
avatarConfig: avatarConfig ?? this.avatarConfig, avatarConfig: avatarConfig ?? this.avatarConfig,
lastSyncAt: lastSyncAt ?? this.lastSyncAt, lastSyncAt: lastSyncAt ?? this.lastSyncAt,
@ -565,6 +611,11 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
if (currentBodyweight.present) { if (currentBodyweight.present) {
map['current_bodyweight'] = Variable<double>(currentBodyweight.value); map['current_bodyweight'] = Variable<double>(currentBodyweight.value);
} }
if (exerciseVariants.present) {
map['exercise_variants'] = Variable<String>($UsersTable
.$converterexerciseVariantsn
.toSql(exerciseVariants.value));
}
if (inventorySettings.present) { if (inventorySettings.present) {
map['inventory_settings'] = Variable<String>($UsersTable map['inventory_settings'] = Variable<String>($UsersTable
.$converterinventorySettingsn .$converterinventorySettingsn
@ -598,6 +649,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
..write('xp: $xp, ') ..write('xp: $xp, ')
..write('level: $level, ') ..write('level: $level, ')
..write('currentBodyweight: $currentBodyweight, ') ..write('currentBodyweight: $currentBodyweight, ')
..write('exerciseVariants: $exerciseVariants, ')
..write('inventorySettings: $inventorySettings, ') ..write('inventorySettings: $inventorySettings, ')
..write('avatarConfig: $avatarConfig, ') ..write('avatarConfig: $avatarConfig, ')
..write('lastSyncAt: $lastSyncAt, ') ..write('lastSyncAt: $lastSyncAt, ')
@ -2452,6 +2504,7 @@ typedef $$UsersTableCreateCompanionBuilder = UsersCompanion Function({
Value<int> xp, Value<int> xp,
Value<int> level, Value<int> level,
Value<double> currentBodyweight, Value<double> currentBodyweight,
Value<Map<String, dynamic>?> exerciseVariants,
Value<Map<String, dynamic>?> inventorySettings, Value<Map<String, dynamic>?> inventorySettings,
Value<Map<String, dynamic>?> avatarConfig, Value<Map<String, dynamic>?> avatarConfig,
Value<DateTime?> lastSyncAt, Value<DateTime?> lastSyncAt,
@ -2466,6 +2519,7 @@ typedef $$UsersTableUpdateCompanionBuilder = UsersCompanion Function({
Value<int> xp, Value<int> xp,
Value<int> level, Value<int> level,
Value<double> currentBodyweight, Value<double> currentBodyweight,
Value<Map<String, dynamic>?> exerciseVariants,
Value<Map<String, dynamic>?> inventorySettings, Value<Map<String, dynamic>?> inventorySettings,
Value<Map<String, dynamic>?> avatarConfig, Value<Map<String, dynamic>?> avatarConfig,
Value<DateTime?> lastSyncAt, Value<DateTime?> lastSyncAt,
@ -2501,6 +2555,12 @@ class $$UsersTableFilterComposer extends Composer<_$AppDatabase, $UsersTable> {
column: $table.currentBodyweight, column: $table.currentBodyweight,
builder: (column) => ColumnFilters(column)); builder: (column) => ColumnFilters(column));
ColumnWithTypeConverterFilters<Map<String, dynamic>?, Map<String, dynamic>,
String>
get exerciseVariants => $composableBuilder(
column: $table.exerciseVariants,
builder: (column) => ColumnWithTypeConverterFilters(column));
ColumnWithTypeConverterFilters<Map<String, dynamic>?, Map<String, dynamic>, ColumnWithTypeConverterFilters<Map<String, dynamic>?, Map<String, dynamic>,
String> String>
get inventorySettings => $composableBuilder( get inventorySettings => $composableBuilder(
@ -2554,6 +2614,10 @@ class $$UsersTableOrderingComposer
column: $table.currentBodyweight, column: $table.currentBodyweight,
builder: (column) => ColumnOrderings(column)); builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get exerciseVariants => $composableBuilder(
column: $table.exerciseVariants,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get inventorySettings => $composableBuilder( ColumnOrderings<String> get inventorySettings => $composableBuilder(
column: $table.inventorySettings, column: $table.inventorySettings,
builder: (column) => ColumnOrderings(column)); builder: (column) => ColumnOrderings(column));
@ -2602,6 +2666,10 @@ class $$UsersTableAnnotationComposer
GeneratedColumn<double> get currentBodyweight => $composableBuilder( GeneratedColumn<double> get currentBodyweight => $composableBuilder(
column: $table.currentBodyweight, builder: (column) => column); column: $table.currentBodyweight, builder: (column) => column);
GeneratedColumnWithTypeConverter<Map<String, dynamic>?, String>
get exerciseVariants => $composableBuilder(
column: $table.exerciseVariants, builder: (column) => column);
GeneratedColumnWithTypeConverter<Map<String, dynamic>?, String> GeneratedColumnWithTypeConverter<Map<String, dynamic>?, String>
get inventorySettings => $composableBuilder( get inventorySettings => $composableBuilder(
column: $table.inventorySettings, builder: (column) => column); column: $table.inventorySettings, builder: (column) => column);
@ -2655,6 +2723,8 @@ class $$UsersTableTableManager extends RootTableManager<
Value<int> xp = const Value.absent(), Value<int> xp = const Value.absent(),
Value<int> level = const Value.absent(), Value<int> level = const Value.absent(),
Value<double> currentBodyweight = const Value.absent(), Value<double> currentBodyweight = const Value.absent(),
Value<Map<String, dynamic>?> exerciseVariants =
const Value.absent(),
Value<Map<String, dynamic>?> inventorySettings = Value<Map<String, dynamic>?> inventorySettings =
const Value.absent(), const Value.absent(),
Value<Map<String, dynamic>?> avatarConfig = const Value.absent(), Value<Map<String, dynamic>?> avatarConfig = const Value.absent(),
@ -2670,6 +2740,7 @@ class $$UsersTableTableManager extends RootTableManager<
xp: xp, xp: xp,
level: level, level: level,
currentBodyweight: currentBodyweight, currentBodyweight: currentBodyweight,
exerciseVariants: exerciseVariants,
inventorySettings: inventorySettings, inventorySettings: inventorySettings,
avatarConfig: avatarConfig, avatarConfig: avatarConfig,
lastSyncAt: lastSyncAt, lastSyncAt: lastSyncAt,
@ -2684,6 +2755,8 @@ class $$UsersTableTableManager extends RootTableManager<
Value<int> xp = const Value.absent(), Value<int> xp = const Value.absent(),
Value<int> level = const Value.absent(), Value<int> level = const Value.absent(),
Value<double> currentBodyweight = const Value.absent(), Value<double> currentBodyweight = const Value.absent(),
Value<Map<String, dynamic>?> exerciseVariants =
const Value.absent(),
Value<Map<String, dynamic>?> inventorySettings = Value<Map<String, dynamic>?> inventorySettings =
const Value.absent(), const Value.absent(),
Value<Map<String, dynamic>?> avatarConfig = const Value.absent(), Value<Map<String, dynamic>?> avatarConfig = const Value.absent(),
@ -2699,6 +2772,7 @@ class $$UsersTableTableManager extends RootTableManager<
xp: xp, xp: xp,
level: level, level: level,
currentBodyweight: currentBodyweight, currentBodyweight: currentBodyweight,
exerciseVariants: exerciseVariants,
inventorySettings: inventorySettings, inventorySettings: inventorySettings,
avatarConfig: avatarConfig, avatarConfig: avatarConfig,
lastSyncAt: lastSyncAt, lastSyncAt: lastSyncAt,

View file

@ -11,6 +11,8 @@ class Users extends Table {
IntColumn get level => integer().withDefault(const Constant(1))(); IntColumn get level => integer().withDefault(const Constant(1))();
RealColumn get currentBodyweight => RealColumn get currentBodyweight =>
real().withDefault(const Constant(70.0))(); real().withDefault(const Constant(70.0))();
TextColumn get exerciseVariants =>
text().map(const MapConverter()).nullable()();
TextColumn get inventorySettings => TextColumn get inventorySettings =>
text().map(const MapConverter()).nullable()(); text().map(const MapConverter()).nullable()();

View file

@ -85,6 +85,7 @@ class ApiClient {
required String password, required String password,
required double bodyweight, required double bodyweight,
required Map<String, dynamic> inventorySettings, required Map<String, dynamic> inventorySettings,
Map<String, dynamic>? exerciseVariants,
}) async { }) async {
try { try {
final response = await _dio.post( final response = await _dio.post(
@ -97,6 +98,7 @@ class ApiClient {
'level': 1, 'level': 1,
'current_bodyweight': bodyweight, 'current_bodyweight': bodyweight,
'inventory_settings': inventorySettings, 'inventory_settings': inventorySettings,
'exercise_variants': exerciseVariants ?? {},
'avatar_config': { 'avatar_config': {
'skin_tone': 'medium', 'skin_tone': 'medium',
'hair_style': 'short_01', 'hair_style': 'short_01',

View file

@ -1,233 +1,3 @@
// import 'dart:convert';
// import 'package:flutter/foundation.dart';
// import 'package:flutter_riverpod/flutter_riverpod.dart';
// import 'package:drift/drift.dart';
// import 'package:flutter_secure_storage/flutter_secure_storage.dart';
// import '../../../../main.dart';
// import '../../../core/constants/app_constants.dart';
// import '../local/app_database.dart';
// import '../repositories/user_repository.dart';
// import 'api_client.dart';
// final syncServiceProvider = Provider<SyncService>((ref) {
// final db = ref.watch(appDatabaseProvider);
// final apiClient = ref.watch(apiClientProvider);
// return SyncService(db: db, apiClient: apiClient);
// });
// class SyncService {
// final AppDatabase db;
// final ApiClient apiClient;
// final _storage = const FlutterSecureStorage();
// bool _isSyncing = false;
// SyncService({required this.db, required this.apiClient});
// Future<void> sync() async {
// if (_isSyncing) return;
// _isSyncing = true;
// try {
// debugPrint('🔄 Starting Sync...');
// final dirtyCycles = await (db.select(db.cycles)
// ..where((c) => c.isDirty.equals(true)))
// .get();
// for (var cycle in dirtyCycles) {
// try {
// if (cycle.serverId == null) {
// debugPrint(
// '📤 Pushing new cycle ${cycle.cycleNumber} to server...');
// final tmsMap = cycle.trainingMaxes
// .map((k, v) => MapEntry(k, (v as num).toDouble()));
// final response = await apiClient.createCycle(tmsMap);
// final newServerId = response['id'];
// await db.transaction(() async {
// await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
// .write(
// CyclesCompanion(
// serverId: Value(newServerId),
// isDirty: const Value(false),
// ),
// );
// final oldLocalIdRef = cycle.id.toString();
// await (db.update(db.workouts)
// ..where((w) => w.cycleId.equals(oldLocalIdRef)))
// .write(
// WorkoutsCompanion(
// cycleId: Value(newServerId),
// isDirty: const Value(true),
// ),
// );
// debugPrint(
// '🔗 Relinked workouts from local cycle $oldLocalIdRef to server $newServerId');
// });
// } else {
// await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
// .write(
// const CyclesCompanion(isDirty: Value(false)),
// );
// }
// } catch (e) {
// debugPrint('❌ Failed to sync cycle ${cycle.id}: $e');
// }
// }
// final dirtyUser = await (db.select(db.users)
// ..where((u) => u.isDirty.equals(true)))
// .getSingleOrNull();
// final dirtyWorkouts = await (db.select(db.workouts)
// ..where((w) => w.isDirty.equals(true)))
// .get();
// final validWorkouts =
// dirtyWorkouts.where((w) => w.cycleId.length > 5).toList();
// final pushData = <String, dynamic>{
// 'workouts': validWorkouts.map((w) {
// return {
// 'id': w.serverId,
// 'local_id': w.id,
// 'cycle_id': w.cycleId,
// 'week': w.week,
// 'day': w.day,
// 'completed_at': w.completedAt?.toIso8601String(),
// 'xp_earned': w.xpEarned,
// 'notes': w.notes,
// 'exercises': w.exercises,
// };
// }).toList(),
// 'user_stats': dirtyUser != null
// ? {
// 'xp': dirtyUser.xp,
// 'level': dirtyUser.level,
// 'current_bodyweight': dirtyUser.currentBodyweight,
// }
// : null,
// };
// final lastSync = await _storage.read(key: AppConstants.keyLastSync);
// debugPrint(
// '☁️ Contacting server (Push: ${validWorkouts.length} workouts)...');
// final response = await apiClient.sync(
// lastSyncTimestamp: lastSync ?? '',
// pushData: pushData,
// );
// await db.transaction(() async {
// if (dirtyUser != null) {
// await (db.update(db.users)..where((u) => u.id.equals(dirtyUser.id)))
// .write(
// const UsersCompanion(isDirty: Value(false)),
// );
// }
// for (var w in validWorkouts) {
// await (db.update(db.workouts)..where((dw) => dw.id.equals(w.id)))
// .write(
// const WorkoutsCompanion(isDirty: Value(false)),
// );
// }
// if (response['pull_data'] != null) {
// if (response['pull_data']['cycles'] != null) {
// final pulledCycles = response['pull_data']['cycles'] as List;
// for (var cJson in pulledCycles) {
// final serverId = cJson['id'] as String;
// final existing = await (db.select(db.cycles)
// ..where((c) => c.serverId.equals(serverId)))
// .getSingleOrNull();
// final tms = cJson['training_maxes'] as Map<String, dynamic>;
// final companion = CyclesCompanion(
// serverId: Value(serverId),
// userId: Value(cJson['user_id']),
// cycleNumber: Value(cJson['cycle_number']),
// startDate: Value(DateTime.parse(cJson['start_date'])),
// endDate: Value(DateTime.tryParse(cJson['end_date'] ?? '')),
// isActive: Value(cJson['is_active'] ?? false),
// trainingMaxes: Value(tms),
// isDirty: const Value(false),
// updatedAt: Value(DateTime.now()),
// createdAt: existing == null
// ? Value(DateTime.now())
// : const Value.absent(),
// );
// if (existing != null) {
// await (db.update(db.cycles)
// ..where((c) => c.id.equals(existing.id)))
// .write(companion);
// } else {
// await db.into(db.cycles).insert(companion);
// }
// }
// }
// if (response['pull_data']['workouts'] != null) {
// final pulledWorkouts = response['pull_data']['workouts'] as List;
// debugPrint(
// '📥 Pulled ${pulledWorkouts.length} workouts from server.');
// for (var wJson in pulledWorkouts) {
// final serverId = wJson['id'] as String;
// final existing = await (db.select(db.workouts)
// ..where((w) => w.serverId.equals(serverId)))
// .getSingleOrNull();
// final companion = WorkoutsCompanion(
// serverId: Value(serverId),
// cycleId: Value(wJson['cycle_id']),
// userId: Value(wJson['user_id']),
// week: Value(wJson['week']),
// day: Value(wJson['day']),
// completedAt:
// Value(DateTime.tryParse(wJson['completed_at'] ?? '')),
// xpEarned: Value(wJson['xp_earned'] ?? 0),
// exercises: Value(wJson['exercises'] ?? []),
// notes: Value(wJson['notes'] ?? ''),
// isDirty: const Value(false),
// updatedAt: Value(DateTime.now()),
// createdAt: existing == null
// ? Value(DateTime.now())
// : const Value.absent(),
// );
// if (existing != null) {
// await (db.update(db.workouts)
// ..where((w) => w.id.equals(existing.id)))
// .write(companion);
// } else {
// await db.into(db.workouts).insert(companion);
// }
// }
// }
// }
// });
// if (response['server_timestamp'] != null) {
// await _storage.write(
// key: AppConstants.keyLastSync,
// value: response['server_timestamp'],
// );
// }
// debugPrint('✅ Sync completed successfully');
// } catch (e, stack) {
// debugPrint('❌ Sync failed: $e');
// debugPrint(stack.toString());
// } finally {
// _isSyncing = false;
// }
// }
// }
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -261,7 +31,6 @@ class SyncService {
try { try {
debugPrint('🔄 Starting Sync...'); debugPrint('🔄 Starting Sync...');
// 1. CYCLES SYNC
final dirtyCycles = await (db.select(db.cycles) final dirtyCycles = await (db.select(db.cycles)
..where((c) => c.isDirty.equals(true))) ..where((c) => c.isDirty.equals(true)))
.get(); .get();
@ -285,7 +54,6 @@ class SyncService {
), ),
); );
// Relink workouts
final oldLocalIdRef = cycle.id.toString(); final oldLocalIdRef = cycle.id.toString();
await (db.update(db.workouts) await (db.update(db.workouts)
..where((w) => w.cycleId.equals(oldLocalIdRef))) ..where((w) => w.cycleId.equals(oldLocalIdRef)))
@ -305,7 +73,6 @@ class SyncService {
} }
} }
// 2. USER & WORKOUTS SYNC
final dirtyUser = await (db.select(db.users) final dirtyUser = await (db.select(db.users)
..where((u) => u.isDirty.equals(true))) ..where((u) => u.isDirty.equals(true)))
.getSingleOrNull(); .getSingleOrNull();
@ -335,6 +102,7 @@ class SyncService {
'xp': dirtyUser.xp, 'xp': dirtyUser.xp,
'level': dirtyUser.level, 'level': dirtyUser.level,
'current_bodyweight': dirtyUser.currentBodyweight, 'current_bodyweight': dirtyUser.currentBodyweight,
'exercise_variants': dirtyUser.exerciseVariants,
} }
: null, : null,
}; };
@ -350,7 +118,6 @@ class SyncService {
); );
await db.transaction(() async { await db.transaction(() async {
// Clean dirty flags
if (dirtyUser != null) { if (dirtyUser != null) {
await (db.update(db.users)..where((u) => u.id.equals(dirtyUser.id))) await (db.update(db.users)..where((u) => u.id.equals(dirtyUser.id)))
.write(const UsersCompanion(isDirty: Value(false))); .write(const UsersCompanion(isDirty: Value(false)));
@ -360,9 +127,7 @@ class SyncService {
.write(const WorkoutsCompanion(isDirty: Value(false))); .write(const WorkoutsCompanion(isDirty: Value(false)));
} }
// PROCESS PULL DATA
if (response['pull_data'] != null) { if (response['pull_data'] != null) {
// Cycles Pull
if (response['pull_data']['cycles'] != null) { if (response['pull_data']['cycles'] != null) {
final pulledCycles = response['pull_data']['cycles'] as List; final pulledCycles = response['pull_data']['cycles'] as List;
for (var cJson in pulledCycles) { for (var cJson in pulledCycles) {
@ -397,7 +162,6 @@ class SyncService {
} }
} }
// Workouts Pull - MIT DUPLIKAT-SCHUTZ
if (response['pull_data']['workouts'] != null) { if (response['pull_data']['workouts'] != null) {
final pulledWorkouts = response['pull_data']['workouts'] as List; final pulledWorkouts = response['pull_data']['workouts'] as List;
debugPrint('📥 Pulled ${pulledWorkouts.length} workouts.'); debugPrint('📥 Pulled ${pulledWorkouts.length} workouts.');
@ -408,13 +172,10 @@ class SyncService {
final week = wJson['week'] as int; final week = wJson['week'] as int;
final day = wJson['day'] as int; final day = wJson['day'] as int;
// 1. Versuch: Match über Server ID
var existing = await (db.select(db.workouts) var existing = await (db.select(db.workouts)
..where((w) => w.serverId.equals(serverId))) ..where((w) => w.serverId.equals(serverId)))
.getSingleOrNull(); .getSingleOrNull();
// 2. Versuch: Match über Logik (Cycle + Week + Day)
// Das verhindert Duplikate, wenn ServerID lokal noch fehlt
if (existing == null) { if (existing == null) {
final candidates = await (db.select(db.workouts) final candidates = await (db.select(db.workouts)
..where((w) => ..where((w) =>

View file

@ -105,6 +105,7 @@ class UserRepository {
required String password, required String password,
required double bodyweight, required double bodyweight,
required Map<String, dynamic> inventorySettings, required Map<String, dynamic> inventorySettings,
Map<String, dynamic>? exerciseVariants,
}) async { }) async {
try { try {
final response = await apiClient.register( final response = await apiClient.register(
@ -112,10 +113,27 @@ class UserRepository {
password: password, password: password,
bodyweight: bodyweight, bodyweight: bodyweight,
inventorySettings: inventorySettings, inventorySettings: inventorySettings,
exerciseVariants: exerciseVariants,
); );
final record = response['record'] ?? response; final record = response['record'] ?? response;
final user = await _saveUserFromApi(record); var user = await _saveUserFromApi(record);
if (exerciseVariants != null && exerciseVariants.isNotEmpty) {
final serverVariants = user.exerciseVariants;
if (serverVariants == null || serverVariants.isEmpty) {
final companion = user.toCompanion(true).copyWith(
exerciseVariants: Value(exerciseVariants),
isDirty: const Value(true),
updatedAt: Value(DateTime.now()),
);
await db.into(db.users).insertOnConflictUpdate(companion);
user = (await (db.select(db.users)
..where((u) => u.id.equals(user.id)))
.getSingle());
}
}
try { try {
await apiClient.login(email, password); await apiClient.login(email, password);
@ -138,6 +156,7 @@ class UserRepository {
currentBodyweight: currentBodyweight:
Value((record['current_bodyweight'] as num?)?.toDouble() ?? 70.0), Value((record['current_bodyweight'] as num?)?.toDouble() ?? 70.0),
inventorySettings: Value(record['inventory_settings'] ?? {}), inventorySettings: Value(record['inventory_settings'] ?? {}),
exerciseVariants: Value(record['exercise_variants'] ?? {}),
avatarConfig: Value(record['avatar_config'] ?? {}), avatarConfig: Value(record['avatar_config'] ?? {}),
lastSyncAt: Value(DateTime.now()), lastSyncAt: Value(DateTime.now()),
isDirty: const Value(false), isDirty: const Value(false),
@ -159,6 +178,7 @@ class UserRepository {
await db.delete(db.users).go(); await db.delete(db.users).go();
await db.delete(db.cycles).go(); await db.delete(db.cycles).go();
await db.delete(db.workouts).go(); await db.delete(db.workouts).go();
await db.delete(db.quests).go();
}); });
} }

View file

@ -2,7 +2,7 @@ import 'dart:math';
import '../entities/workout_set.dart'; import '../entities/workout_set.dart';
import '../../../core/constants/app_constants.dart'; import '../../../core/constants/app_constants.dart';
enum ExerciseType { squat, pullup, dip } enum ExerciseType { squat, pullup, dip, row, bench }
class WendlerCalculator { class WendlerCalculator {
static const Map<int, List<double>> weekPercentages = { static const Map<int, List<double>> weekPercentages = {
@ -51,8 +51,38 @@ class WendlerCalculator {
return sets; return sets;
} }
static List<WorkoutSet> generateLinearSets({
required double trainingMax,
required ExerciseType exerciseType,
required double currentBodyweight,
int setsCount = 3,
int repsCount = 5,
}) {
final sets = <WorkoutSet>[];
final targetTotal = _roundWeight(trainingMax, exerciseType);
double plateWeight = 0;
for (int i = 0; i < setsCount; i++) {
sets.add(WorkoutSet(
setNumber: i + 1,
targetPercentage: 100,
targetWeightTotal: targetTotal,
plateWeight: plateWeight,
repsTarget: repsCount,
repsActual: 0,
isAmrap: (i == setsCount - 1),
));
}
return sets;
}
static double _roundWeight(double weight, ExerciseType type) { static double _roundWeight(double weight, ExerciseType type) {
final step = type == ExerciseType.squat final step = (type == ExerciseType.squat ||
type == ExerciseType.row ||
type == ExerciseType.bench)
? AppConstants.squatRoundingStep ? AppConstants.squatRoundingStep
: AppConstants.calisthenicsRoundingStep; : AppConstants.calisthenicsRoundingStep;
return (weight / step).floor() * step; return (weight / step).floor() * step;