feat: Completed with Starter MainLifts
This commit is contained in:
parent
311d764a4d
commit
2609446e9a
15 changed files with 642 additions and 491 deletions
|
|
@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
|
|||
|
||||
class AppConstants {
|
||||
// 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';
|
||||
|
||||
// Wendler 5/3/1 Constants
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import 'dart:convert';
|
||||
// import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../../../../shared/data/local/app_database.dart';
|
||||
import '../../../../shared/data/repositories/user_repository.dart';
|
||||
|
|
@ -54,27 +54,64 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
required int day,
|
||||
required Map<String, double> trainingMaxes,
|
||||
required double bodyweight,
|
||||
required UserCollection user,
|
||||
}) {
|
||||
final exercises = <Exercise>[];
|
||||
final variants = user.exerciseVariants ?? {};
|
||||
|
||||
void addExercise(String id, String name, ExerciseType type, bool isMain) {
|
||||
final tm = trainingMaxes[id] ?? 0.0;
|
||||
(String, String, ExerciseType) resolveVariant(String slot, String defaultId,
|
||||
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;
|
||||
|
||||
if (isMain) {
|
||||
sets = WendlerCalculator.generateSets(
|
||||
week: week,
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: bodyweight,
|
||||
);
|
||||
if (type == ExerciseType.row || type == ExerciseType.bench) {
|
||||
sets = WendlerCalculator.generateLinearSets(
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: user.currentBodyweight);
|
||||
} else {
|
||||
sets = WendlerCalculator.generateSets(
|
||||
week: week,
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: user.currentBodyweight,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (week == 4) return;
|
||||
if (week == 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == ExerciseType.row || type == ExerciseType.bench) return;
|
||||
|
||||
sets = WendlerCalculator.generateFSLSets(
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: bodyweight,
|
||||
currentBodyweight: user.currentBodyweight,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -82,21 +119,23 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
exercises.add(Exercise(
|
||||
exerciseId: id,
|
||||
exerciseName: isMain ? name : '$name (FSL)',
|
||||
bodyweightAtSession: bodyweight,
|
||||
bodyweightAtSession: user.currentBodyweight,
|
||||
sets: sets,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (day == 1) {
|
||||
addExercise('squat', 'Back Squat', ExerciseType.squat, true);
|
||||
addExercise('pullup', 'Weighted Pull-up', ExerciseType.pullup, false);
|
||||
addExercise('legs', 'squat', 'Back Squat', ExerciseType.squat, true);
|
||||
addExercise(
|
||||
'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, false);
|
||||
} else if (day == 2) {
|
||||
addExercise('dip', 'Weighted Dip', ExerciseType.dip, true);
|
||||
addExercise('squat', 'Back Squat', ExerciseType.squat, false);
|
||||
addExercise('push', 'dip', 'Weighted Dip', ExerciseType.dip, true);
|
||||
addExercise('legs', 'squat', 'Back Squat', ExerciseType.squat, false);
|
||||
} else if (day == 3) {
|
||||
addExercise('pullup', 'Weighted Pull-up', ExerciseType.pullup, true);
|
||||
addExercise('dip', 'Weighted Dip', ExerciseType.dip, false);
|
||||
addExercise(
|
||||
'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, true);
|
||||
addExercise('push', 'dip', 'Weighted Dip', ExerciseType.dip, false);
|
||||
}
|
||||
|
||||
return exercises;
|
||||
|
|
@ -152,11 +191,11 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
|
||||
if (workout == null) {
|
||||
final exercises = _generateExercises(
|
||||
week: targetWeek,
|
||||
day: targetDay,
|
||||
trainingMaxes: trainingMaxes,
|
||||
bodyweight: user.currentBodyweight,
|
||||
);
|
||||
week: targetWeek,
|
||||
day: targetDay,
|
||||
trainingMaxes: trainingMaxes,
|
||||
bodyweight: user.currentBodyweight,
|
||||
user: user);
|
||||
|
||||
final userId = user.serverId ?? user.id.toString();
|
||||
|
||||
|
|
@ -231,16 +270,9 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
child: Image.asset(
|
||||
bgItem.assetPath,
|
||||
fit: BoxFit.cover,
|
||||
// Key hinzufügen, damit Flutter einen sanften Übergang animieren kann (optional)
|
||||
key: ValueKey(bgItem.assetPath),
|
||||
),
|
||||
),
|
||||
// Positioned.fill(
|
||||
// child: Image.asset(
|
||||
// AssetPaths.bgStreetParkDay,
|
||||
// fit: BoxFit.cover,
|
||||
// ),
|
||||
// ),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -248,8 +280,10 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.6),
|
||||
Colors.black.withOpacity(0.85),
|
||||
// Colors.black.withOpacity(0.6),
|
||||
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)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
// color: Colors.black.withOpacity(0.2),
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
|
|
@ -400,6 +435,10 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
// extension on Object {
|
||||
// operator [](String other) {}
|
||||
// }
|
||||
|
||||
class _StatBox extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
|
@ -417,7 +456,8 @@ class _StatBox extends StatelessWidget {
|
|||
color: AppTheme.surfaceColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppTheme.primaryColor.withOpacity(0.3),
|
||||
// color: AppTheme.primaryColor.withOpacity(0.3),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
|
|
|
|||
|
|
@ -16,19 +16,19 @@ class QuestBoardWidget extends ConsumerWidget {
|
|||
stream: questRepo.watchQuests(),
|
||||
builder: (context, snapshot) {
|
||||
final allQuests = snapshot.data ?? [];
|
||||
// Nur aktive Dailies anzeigen, max 3
|
||||
final dailies = allQuests
|
||||
.where((q) => q.type == 'daily' && !q.isClaimed)
|
||||
final activeQuests = allQuests
|
||||
.where(
|
||||
(q) => !q.isClaimed && (q.type == 'daily' || q.type == 'story'))
|
||||
.take(3)
|
||||
.toList();
|
||||
|
||||
if (dailies.isEmpty) return const SizedBox.shrink(); // Ausblenden wenn leer
|
||||
if (activeQuests.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surfaceColor, // Oder ein "Holz" Texture für RPG Look
|
||||
color: AppTheme.surfaceColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white10),
|
||||
),
|
||||
|
|
@ -41,19 +41,19 @@ class QuestBoardWidget extends ConsumerWidget {
|
|||
Text(
|
||||
'DAILY BOUNTIES',
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: AppTheme.secondaryColor,
|
||||
letterSpacing: 1.5,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
color: AppTheme.secondaryColor,
|
||||
letterSpacing: 1.5,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
GestureDetector(
|
||||
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),
|
||||
...dailies.map((q) => _MiniQuestRow(quest: q)).toList(),
|
||||
...activeQuests.map((q) => _MiniQuestRow(quest: q)).toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
|
|||
final inventorySettings =
|
||||
onboardingData['inventory_settings'] as Map<String, dynamic>;
|
||||
|
||||
final exerciseVariants =
|
||||
onboardingData['exercise_variants'] as Map<String, dynamic>?;
|
||||
var user = await userRepo.getLocalUser();
|
||||
|
||||
if (user == null) {
|
||||
|
|
@ -39,6 +41,7 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
|
|||
password: onboardingData['password'] ?? '',
|
||||
bodyweight: onboardingData['bodyweight'] ?? 80.0,
|
||||
inventorySettings: inventorySettings,
|
||||
exerciseVariants: exerciseVariants,
|
||||
);
|
||||
} else {
|
||||
user = user.copyWith(
|
||||
|
|
|
|||
|
|
@ -17,12 +17,17 @@ class StrengthTestScreen extends ConsumerStatefulWidget {
|
|||
class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
final _squatWeightController = TextEditingController(text: '100');
|
||||
final _squatWeightController = TextEditingController(text: '60');
|
||||
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 _dipRepsController = TextEditingController(text: '10');
|
||||
final _benchWeightController = TextEditingController(text: '40');
|
||||
final _pushRepsController = TextEditingController(text: '5');
|
||||
|
||||
Map<String, double> _calculated1RMs = {};
|
||||
Map<String, double> _calculatedTMs = {};
|
||||
|
|
@ -37,10 +42,10 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
void dispose() {
|
||||
_squatWeightController.dispose();
|
||||
_squatRepsController.dispose();
|
||||
_pullupWeightController.dispose();
|
||||
_pullupRepsController.dispose();
|
||||
_pullWeightController.dispose();
|
||||
_pullRepsController.dispose();
|
||||
_dipWeightController.dispose();
|
||||
_dipRepsController.dispose();
|
||||
_pushRepsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -52,28 +57,40 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
final squat1RM = WendlerCalculator.calculate1RM(squatWeight, squatReps);
|
||||
final squatTM = WendlerCalculator.calculateTrainingMax(squat1RM);
|
||||
|
||||
final pullupAdditional = double.tryParse(_pullupWeightController.text) ?? 0;
|
||||
final pullupReps = int.tryParse(_pullupRepsController.text) ?? 1;
|
||||
final pullupTotal = bodyweight + pullupAdditional;
|
||||
final pullup1RM = WendlerCalculator.calculate1RM(pullupTotal, pullupReps);
|
||||
final pullupTM = WendlerCalculator.calculateTrainingMax(pullup1RM);
|
||||
double pull1RM = 0.0;
|
||||
if (_canDoPullup) {
|
||||
final added = double.tryParse(_pullWeightController.text) ?? 0;
|
||||
final reps = int.tryParse(_pullRepsController.text) ?? 1;
|
||||
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;
|
||||
final dipReps = int.tryParse(_dipRepsController.text) ?? 1;
|
||||
final dipTotal = bodyweight + dipAdditional;
|
||||
final dip1RM = WendlerCalculator.calculate1RM(dipTotal, dipReps);
|
||||
final dipTM = WendlerCalculator.calculateTrainingMax(dip1RM);
|
||||
double push1RM = 0.0;
|
||||
if (_canDoDip) {
|
||||
final added = double.tryParse(_dipWeightController.text) ?? 0;
|
||||
final reps = int.tryParse(_pushRepsController.text) ?? 1;
|
||||
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(() {
|
||||
_calculated1RMs = {
|
||||
'squat': squat1RM,
|
||||
'pullup': pullup1RM,
|
||||
'dip': dip1RM,
|
||||
'pullup': pull1RM,
|
||||
'dip': push1RM,
|
||||
};
|
||||
_calculatedTMs = {
|
||||
'squat': squatTM,
|
||||
'pullup': pullupTM,
|
||||
'dip': dipTM,
|
||||
'pullup': pullTM,
|
||||
'dip': pushTM,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -81,9 +98,15 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
void _handleContinue() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
final variants = <String, String>{
|
||||
'pull': _canDoPullup ? 'pullup' : 'row',
|
||||
'push': _canDoDip ? 'dip' : 'bench',
|
||||
};
|
||||
|
||||
ref.read(onboardingDataProvider.notifier).update((state) => {
|
||||
...state,
|
||||
'training_maxes': _calculatedTMs,
|
||||
'exercise_variants': variants,
|
||||
});
|
||||
|
||||
context.go('/onboarding/inventory');
|
||||
|
|
@ -91,8 +114,6 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bodyweight = ref.watch(onboardingDataProvider)['bodyweight'] ?? 80.0;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Strength Test'),
|
||||
|
|
@ -121,47 +142,70 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'We need to assess your current power level to assign the correct monsters.', // Flavor
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Enter your recent best performance for each exercise',
|
||||
'We need to assess your current power level to assign the correct monsters.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
_ExerciseCard(
|
||||
title: 'Leg Strength',
|
||||
exerciseName: 'Back Squat',
|
||||
icon: Icons.accessibility_new,
|
||||
weightController: _squatWeightController,
|
||||
repsController: _squatRepsController,
|
||||
isBodyweight: false,
|
||||
calculated1RM: _calculated1RMs['squat'] ?? 0,
|
||||
calculatedTM: _calculatedTMs['squat'] ?? 0,
|
||||
onChanged: _calculateAll,
|
||||
result1RM: _calculated1RMs['squat'] ?? 0,
|
||||
resultTM: _calculatedTMs['squat'] ?? 0,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_ExerciseCard(
|
||||
exerciseName: 'Weighted Pull-up',
|
||||
_AdaptiveExerciseCard(
|
||||
slotTitle: 'Pull Strength',
|
||||
primaryName: 'Weighted Pull-up',
|
||||
secondaryName: 'Pendlay Row',
|
||||
icon: Icons.north,
|
||||
weightController: _pullupWeightController,
|
||||
repsController: _pullupRepsController,
|
||||
isBodyweight: true,
|
||||
bodyweight: bodyweight,
|
||||
calculated1RM: _calculated1RMs['pullup'] ?? 0,
|
||||
calculatedTM: _calculatedTMs['pullup'] ?? 0,
|
||||
isCapable: _canDoPullup,
|
||||
onToggleCapability: (val) {
|
||||
setState(() {
|
||||
_canDoPullup = val;
|
||||
_pullWeightController.text = '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,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_ExerciseCard(
|
||||
exerciseName: 'Weighted Dip',
|
||||
_AdaptiveExerciseCard(
|
||||
slotTitle: 'Push Strength',
|
||||
primaryName: 'Weighted Dip',
|
||||
secondaryName: 'Bench Press',
|
||||
icon: Icons.south,
|
||||
weightController: _dipWeightController,
|
||||
repsController: _dipRepsController,
|
||||
isBodyweight: true,
|
||||
bodyweight: bodyweight,
|
||||
calculated1RM: _calculated1RMs['dip'] ?? 0,
|
||||
calculatedTM: _calculatedTMs['dip'] ?? 0,
|
||||
isCapable: _canDoDip,
|
||||
onToggleCapability: (val) {
|
||||
setState(() {
|
||||
_canDoDip = val;
|
||||
_dipWeightController.text = '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,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
|
@ -171,19 +215,16 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppTheme.primaryColor.withOpacity(0.3),
|
||||
),
|
||||
color: AppTheme.primaryColor.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.info_outline,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
const Icon(Icons.info_outline,
|
||||
color: AppTheme.primaryColor),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
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)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
|
|
@ -208,25 +249,25 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
}
|
||||
|
||||
class _ExerciseCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String exerciseName;
|
||||
final IconData icon;
|
||||
final TextEditingController weightController;
|
||||
final TextEditingController repsController;
|
||||
final bool isBodyweight;
|
||||
final double bodyweight;
|
||||
final double calculated1RM;
|
||||
final double calculatedTM;
|
||||
final double result1RM;
|
||||
final double resultTM;
|
||||
final VoidCallback onChanged;
|
||||
|
||||
const _ExerciseCard({
|
||||
required this.title,
|
||||
required this.exerciseName,
|
||||
required this.icon,
|
||||
required this.weightController,
|
||||
required this.repsController,
|
||||
this.isBodyweight = false,
|
||||
this.bodyweight = 0,
|
||||
required this.calculated1RM,
|
||||
required this.calculatedTM,
|
||||
required this.isBodyweight,
|
||||
required this.result1RM,
|
||||
required this.resultTM,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
|
|
@ -238,27 +279,27 @@ class _ExerciseCard extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Text(title.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
color: AppTheme.primaryColor.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: Icon(icon, color: AppTheme.primaryColor),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
exerciseName,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text(exerciseName,
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -268,21 +309,14 @@ class _ExerciseCard extends StatelessWidget {
|
|||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'^\d+\.?\d{0,2}')),
|
||||
RegExp(r'^\d+\.?\d{0,2}'))
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
labelText: isBodyweight
|
||||
? 'Additional Weight (kg)'
|
||||
: 'Weight (kg)',
|
||||
isDense: true,
|
||||
),
|
||||
labelText:
|
||||
isBodyweight ? 'Add. Weight (kg)' : 'Weight (kg)',
|
||||
isDense: true),
|
||||
onChanged: (_) => onChanged(),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
validator: (v) => v!.isEmpty ? 'Required' : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
|
@ -290,56 +324,17 @@ class _ExerciseCard extends StatelessWidget {
|
|||
child: TextFormField(
|
||||
controller: repsController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Reps',
|
||||
isDense: true,
|
||||
),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Reps', isDense: true),
|
||||
onChanged: (_) => onChanged(),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Required';
|
||||
}
|
||||
final reps = int.tryParse(value);
|
||||
if (reps == null || reps < 1 || reps > 20) {
|
||||
return '1-20';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
validator: (v) => v!.isEmpty ? 'Required' : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ResultBox(rm: result1RM, tm: resultTM),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -347,36 +342,176 @@ class _ExerciseCard extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _ResultRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final bool highlight;
|
||||
class _AdaptiveExerciseCard extends StatelessWidget {
|
||||
final String slotTitle;
|
||||
final String primaryName;
|
||||
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({
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.highlight = false,
|
||||
const _AdaptiveExerciseCard({
|
||||
required this.slotTitle,
|
||||
required this.primaryName,
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
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: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: highlight ? FontWeight.bold : null,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Est. 1RM'),
|
||||
Text('${rm.toStringAsFixed(1)} kg',
|
||||
style: Theme.of(context).textTheme.bodyLarge),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: highlight ? AppTheme.primaryColor : null,
|
||||
fontWeight: highlight ? FontWeight.bold : null,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Training Max (90%)'),
|
||||
Text('${tm.toStringAsFixed(1)} kg',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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 '../../../../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/user_repository.dart';
|
||||
import '../../../../shared/data/repositories/workout_repository.dart';
|
||||
|
|
@ -22,7 +23,7 @@ class StatsScreen extends ConsumerStatefulWidget {
|
|||
class _StatsScreenState extends ConsumerState<StatsScreen> {
|
||||
bool _isLoading = false;
|
||||
|
||||
String _selectedExercise = 'squat'; // squat, pullup, dip
|
||||
String _selectedExercise = 'squat';
|
||||
String _selectedRange = '3m'; // 1m, 3m, 1y, all
|
||||
List<StatsDataPoint> _chartData = [];
|
||||
bool _isChartLoading = true;
|
||||
|
|
@ -175,6 +176,7 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cycleRepo = ref.watch(cycleRepositoryProvider);
|
||||
final userRepo = ref.watch(userRepositoryProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -184,14 +186,38 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
|||
onPressed: () => context.go('/hub'),
|
||||
),
|
||||
),
|
||||
body: FutureBuilder<CycleCollection?>(
|
||||
future: cycleRepo.getCurrentCycle(),
|
||||
body: FutureBuilder<List<dynamic>>(
|
||||
future: Future.wait([
|
||||
cycleRepo.getCurrentCycle(),
|
||||
userRepo.getLocalUser(),
|
||||
]),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
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(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
|
@ -201,6 +227,7 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
|||
if (currentCycle != null) ...[
|
||||
_CurrentCycleCard(
|
||||
cycle: currentCycle,
|
||||
user: user,
|
||||
onFinish: _isLoading
|
||||
? null
|
||||
: () => _handleFinishCycle(currentCycle),
|
||||
|
|
@ -226,15 +253,17 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
_FilterChip(
|
||||
label: 'Pull-up',
|
||||
isSelected: _selectedExercise == 'pullup',
|
||||
onTap: () => _onFilterChanged('pullup', _selectedRange),
|
||||
label: getLabel(pullVariant),
|
||||
isSelected: _selectedExercise == pullVariant,
|
||||
onTap: () =>
|
||||
_onFilterChanged(pullVariant, _selectedRange),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_FilterChip(
|
||||
label: 'Dip',
|
||||
isSelected: _selectedExercise == 'dip',
|
||||
onTap: () => _onFilterChanged('dip', _selectedRange),
|
||||
label: getLabel(pushVariant),
|
||||
isSelected: _selectedExercise == pushVariant,
|
||||
onTap: () =>
|
||||
_onFilterChanged(pushVariant, _selectedRange),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -256,14 +285,35 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
|||
|
||||
class _CurrentCycleCard extends StatelessWidget {
|
||||
final CycleCollection cycle;
|
||||
final UserCollection user;
|
||||
final VoidCallback? onFinish;
|
||||
|
||||
const _CurrentCycleCard({required this.cycle, required this.onFinish});
|
||||
const _CurrentCycleCard(
|
||||
{required this.cycle, required this.user, required this.onFinish});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Drift: Direct access
|
||||
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(
|
||||
child: Padding(
|
||||
|
|
@ -303,8 +353,9 @@ class _CurrentCycleCard extends StatelessWidget {
|
|||
style: Theme.of(context).textTheme.labelLarge),
|
||||
const SizedBox(height: 16),
|
||||
_StatRow(label: 'Squat', value: '${tms['squat']} kg'),
|
||||
_StatRow(label: 'Pull-up', value: '${tms['pullup']} kg'),
|
||||
_StatRow(label: 'Dip', value: '${tms['dip']} kg'),
|
||||
_StatRow(
|
||||
label: getLabel(pullVariant), value: '${tms['pullup']} kg'),
|
||||
_StatRow(label: getLabel(pushVariant), value: '${tms['dip']} kg'),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'dart:async';
|
|||
|
||||
import '../../../../core/constants/asset_paths.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../shared/data/local/app_database.dart';
|
||||
import '../../../../shared/domain/entities/exercise.dart';
|
||||
import '../../../../shared/domain/entities/workout_set.dart';
|
||||
import '../../../../shared/domain/logic/wendler_calculator.dart';
|
||||
|
|
@ -62,17 +63,52 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
case 'squat':
|
||||
return AssetPaths.enemyIronGolem;
|
||||
case 'pullup':
|
||||
case 'row':
|
||||
return AssetPaths.enemyGravityDemon;
|
||||
case 'dip':
|
||||
case 'bench':
|
||||
return AssetPaths.enemyPressurePhantom;
|
||||
default:
|
||||
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) {
|
||||
case 1:
|
||||
final pull = getVariant(
|
||||
'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup);
|
||||
return [
|
||||
{
|
||||
'id': 'squat',
|
||||
|
|
@ -80,21 +116,13 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
'type': ExerciseType.squat,
|
||||
'isMain': true
|
||||
},
|
||||
{
|
||||
'id': 'pullup',
|
||||
'name': 'Weighted Pull-up',
|
||||
'type': ExerciseType.pullup,
|
||||
'isMain': false
|
||||
},
|
||||
{...pull, 'isMain': false},
|
||||
];
|
||||
case 2:
|
||||
final push =
|
||||
getVariant('push', 'dip', 'Weighted Dip', ExerciseType.dip);
|
||||
return [
|
||||
{
|
||||
'id': 'dip',
|
||||
'name': 'Weighted Dip',
|
||||
'type': ExerciseType.dip,
|
||||
'isMain': true
|
||||
},
|
||||
{...push, 'isMain': true},
|
||||
{
|
||||
'id': 'squat',
|
||||
'name': 'Back Squat',
|
||||
|
|
@ -103,19 +131,13 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
},
|
||||
];
|
||||
case 3:
|
||||
final pull = getVariant(
|
||||
'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup);
|
||||
final push =
|
||||
getVariant('push', 'dip', 'Weighted Dip', ExerciseType.dip);
|
||||
return [
|
||||
{
|
||||
'id': 'pullup',
|
||||
'name': 'Weighted Pull-up',
|
||||
'type': ExerciseType.pullup,
|
||||
'isMain': true
|
||||
},
|
||||
{
|
||||
'id': 'dip',
|
||||
'name': 'Weighted Dip',
|
||||
'type': ExerciseType.dip,
|
||||
'isMain': false
|
||||
},
|
||||
{...pull, 'isMain': true},
|
||||
{...push, 'isMain': false},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
|
|
@ -135,7 +157,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
}
|
||||
|
||||
final exercises = <Exercise>[];
|
||||
final exerciseConfigs = _getExerciseConfig(widget.day);
|
||||
final exerciseConfigs = _getExerciseConfig(widget.day, user);
|
||||
|
||||
for (final config in exerciseConfigs) {
|
||||
final id = config['id'] as String;
|
||||
|
|
@ -143,7 +165,11 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
final type = config['type'] as ExerciseType;
|
||||
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 = [];
|
||||
|
||||
if (isMain) {
|
||||
|
|
@ -454,7 +480,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
.take(_currentSetIndex)
|
||||
.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
|
||||
? currentExercise.bodyweightAtSession
|
||||
: (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0;
|
||||
|
|
@ -480,7 +509,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
barWeight: barWeight,
|
||||
availablePlates: platesList,
|
||||
availableBands: availableBands,
|
||||
isTwoSided: !isBodyweight,
|
||||
isTwoSided: isTwoSided,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
|
|
@ -764,7 +793,9 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
else
|
||||
PlateVisualizer(
|
||||
plateConfiguration: plateResult.plateConfiguration,
|
||||
isTwoSided: currentExercise.exerciseId == 'squat',
|
||||
isTwoSided: currentExercise.exerciseId == 'squat' ||
|
||||
currentExercise.exerciseId == 'row' ||
|
||||
currentExercise.exerciseId == 'bench',
|
||||
exerciseName: currentExercise.exerciseName,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ class AppDatabase extends _$AppDatabase {
|
|||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 2;
|
||||
int get schemaVersion => 3;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onCreate: (Migrator m) async {
|
||||
await m.createAll();
|
||||
},
|
||||
onUpgrade: (Migrator m, int from, int to) async {
|
||||
if (from < 2) {
|
||||
await m.createTable(quests);
|
||||
}
|
||||
if (from < 3) {
|
||||
await m.addColumn(users, users.exerciseVariants);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,13 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserCollection> {
|
|||
requiredDuringInsert: false,
|
||||
defaultValue: const Constant(70.0));
|
||||
@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>
|
||||
inventorySettings = GeneratedColumn<String>(
|
||||
'inventory_settings', aliasedName, true,
|
||||
|
|
@ -107,6 +114,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserCollection> {
|
|||
xp,
|
||||
level,
|
||||
currentBodyweight,
|
||||
exerciseVariants,
|
||||
inventorySettings,
|
||||
avatarConfig,
|
||||
lastSyncAt,
|
||||
|
|
@ -187,6 +195,9 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserCollection> {
|
|||
.read(DriftSqlType.int, data['${effectivePrefix}level'])!,
|
||||
currentBodyweight: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.double, data['${effectivePrefix}current_bodyweight'])!,
|
||||
exerciseVariants: $UsersTable.$converterexerciseVariantsn.fromSql(
|
||||
attachedDatabase.typeMapping.read(DriftSqlType.string,
|
||||
data['${effectivePrefix}exercise_variants'])),
|
||||
inventorySettings: $UsersTable.$converterinventorySettingsn.fromSql(
|
||||
attachedDatabase.typeMapping.read(DriftSqlType.string,
|
||||
data['${effectivePrefix}inventory_settings'])),
|
||||
|
|
@ -209,6 +220,11 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserCollection> {
|
|||
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>
|
||||
$converterinventorySettings = const MapConverter();
|
||||
static TypeConverter<Map<String, dynamic>?, String?>
|
||||
|
|
@ -227,6 +243,7 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
|
|||
final int xp;
|
||||
final int level;
|
||||
final double currentBodyweight;
|
||||
final Map<String, dynamic>? exerciseVariants;
|
||||
final Map<String, dynamic>? inventorySettings;
|
||||
final Map<String, dynamic>? avatarConfig;
|
||||
final DateTime? lastSyncAt;
|
||||
|
|
@ -240,6 +257,7 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
|
|||
required this.xp,
|
||||
required this.level,
|
||||
required this.currentBodyweight,
|
||||
this.exerciseVariants,
|
||||
this.inventorySettings,
|
||||
this.avatarConfig,
|
||||
this.lastSyncAt,
|
||||
|
|
@ -257,6 +275,10 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
|
|||
map['xp'] = Variable<int>(xp);
|
||||
map['level'] = Variable<int>(level);
|
||||
map['current_bodyweight'] = Variable<double>(currentBodyweight);
|
||||
if (!nullToAbsent || exerciseVariants != null) {
|
||||
map['exercise_variants'] = Variable<String>(
|
||||
$UsersTable.$converterexerciseVariantsn.toSql(exerciseVariants));
|
||||
}
|
||||
if (!nullToAbsent || inventorySettings != null) {
|
||||
map['inventory_settings'] = Variable<String>(
|
||||
$UsersTable.$converterinventorySettingsn.toSql(inventorySettings));
|
||||
|
|
@ -284,6 +306,9 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
|
|||
xp: Value(xp),
|
||||
level: Value(level),
|
||||
currentBodyweight: Value(currentBodyweight),
|
||||
exerciseVariants: exerciseVariants == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(exerciseVariants),
|
||||
inventorySettings: inventorySettings == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(inventorySettings),
|
||||
|
|
@ -309,6 +334,8 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
|
|||
xp: serializer.fromJson<int>(json['xp']),
|
||||
level: serializer.fromJson<int>(json['level']),
|
||||
currentBodyweight: serializer.fromJson<double>(json['currentBodyweight']),
|
||||
exerciseVariants:
|
||||
serializer.fromJson<Map<String, dynamic>?>(json['exerciseVariants']),
|
||||
inventorySettings:
|
||||
serializer.fromJson<Map<String, dynamic>?>(json['inventorySettings']),
|
||||
avatarConfig:
|
||||
|
|
@ -329,6 +356,8 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
|
|||
'xp': serializer.toJson<int>(xp),
|
||||
'level': serializer.toJson<int>(level),
|
||||
'currentBodyweight': serializer.toJson<double>(currentBodyweight),
|
||||
'exerciseVariants':
|
||||
serializer.toJson<Map<String, dynamic>?>(exerciseVariants),
|
||||
'inventorySettings':
|
||||
serializer.toJson<Map<String, dynamic>?>(inventorySettings),
|
||||
'avatarConfig': serializer.toJson<Map<String, dynamic>?>(avatarConfig),
|
||||
|
|
@ -346,6 +375,7 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
|
|||
int? xp,
|
||||
int? level,
|
||||
double? currentBodyweight,
|
||||
Value<Map<String, dynamic>?> exerciseVariants = const Value.absent(),
|
||||
Value<Map<String, dynamic>?> inventorySettings = const Value.absent(),
|
||||
Value<Map<String, dynamic>?> avatarConfig = const Value.absent(),
|
||||
Value<DateTime?> lastSyncAt = const Value.absent(),
|
||||
|
|
@ -359,6 +389,9 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
|
|||
xp: xp ?? this.xp,
|
||||
level: level ?? this.level,
|
||||
currentBodyweight: currentBodyweight ?? this.currentBodyweight,
|
||||
exerciseVariants: exerciseVariants.present
|
||||
? exerciseVariants.value
|
||||
: this.exerciseVariants,
|
||||
inventorySettings: inventorySettings.present
|
||||
? inventorySettings.value
|
||||
: this.inventorySettings,
|
||||
|
|
@ -379,6 +412,9 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
|
|||
currentBodyweight: data.currentBodyweight.present
|
||||
? data.currentBodyweight.value
|
||||
: this.currentBodyweight,
|
||||
exerciseVariants: data.exerciseVariants.present
|
||||
? data.exerciseVariants.value
|
||||
: this.exerciseVariants,
|
||||
inventorySettings: data.inventorySettings.present
|
||||
? data.inventorySettings.value
|
||||
: this.inventorySettings,
|
||||
|
|
@ -402,6 +438,7 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
|
|||
..write('xp: $xp, ')
|
||||
..write('level: $level, ')
|
||||
..write('currentBodyweight: $currentBodyweight, ')
|
||||
..write('exerciseVariants: $exerciseVariants, ')
|
||||
..write('inventorySettings: $inventorySettings, ')
|
||||
..write('avatarConfig: $avatarConfig, ')
|
||||
..write('lastSyncAt: $lastSyncAt, ')
|
||||
|
|
@ -420,6 +457,7 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
|
|||
xp,
|
||||
level,
|
||||
currentBodyweight,
|
||||
exerciseVariants,
|
||||
inventorySettings,
|
||||
avatarConfig,
|
||||
lastSyncAt,
|
||||
|
|
@ -436,6 +474,7 @@ class UserCollection extends DataClass implements Insertable<UserCollection> {
|
|||
other.xp == this.xp &&
|
||||
other.level == this.level &&
|
||||
other.currentBodyweight == this.currentBodyweight &&
|
||||
other.exerciseVariants == this.exerciseVariants &&
|
||||
other.inventorySettings == this.inventorySettings &&
|
||||
other.avatarConfig == this.avatarConfig &&
|
||||
other.lastSyncAt == this.lastSyncAt &&
|
||||
|
|
@ -451,6 +490,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
|
|||
final Value<int> xp;
|
||||
final Value<int> level;
|
||||
final Value<double> currentBodyweight;
|
||||
final Value<Map<String, dynamic>?> exerciseVariants;
|
||||
final Value<Map<String, dynamic>?> inventorySettings;
|
||||
final Value<Map<String, dynamic>?> avatarConfig;
|
||||
final Value<DateTime?> lastSyncAt;
|
||||
|
|
@ -464,6 +504,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
|
|||
this.xp = const Value.absent(),
|
||||
this.level = const Value.absent(),
|
||||
this.currentBodyweight = const Value.absent(),
|
||||
this.exerciseVariants = const Value.absent(),
|
||||
this.inventorySettings = const Value.absent(),
|
||||
this.avatarConfig = const Value.absent(),
|
||||
this.lastSyncAt = const Value.absent(),
|
||||
|
|
@ -478,6 +519,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
|
|||
this.xp = const Value.absent(),
|
||||
this.level = const Value.absent(),
|
||||
this.currentBodyweight = const Value.absent(),
|
||||
this.exerciseVariants = const Value.absent(),
|
||||
this.inventorySettings = const Value.absent(),
|
||||
this.avatarConfig = const Value.absent(),
|
||||
this.lastSyncAt = const Value.absent(),
|
||||
|
|
@ -492,6 +534,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
|
|||
Expression<int>? xp,
|
||||
Expression<int>? level,
|
||||
Expression<double>? currentBodyweight,
|
||||
Expression<String>? exerciseVariants,
|
||||
Expression<String>? inventorySettings,
|
||||
Expression<String>? avatarConfig,
|
||||
Expression<DateTime>? lastSyncAt,
|
||||
|
|
@ -506,6 +549,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
|
|||
if (xp != null) 'xp': xp,
|
||||
if (level != null) 'level': level,
|
||||
if (currentBodyweight != null) 'current_bodyweight': currentBodyweight,
|
||||
if (exerciseVariants != null) 'exercise_variants': exerciseVariants,
|
||||
if (inventorySettings != null) 'inventory_settings': inventorySettings,
|
||||
if (avatarConfig != null) 'avatar_config': avatarConfig,
|
||||
if (lastSyncAt != null) 'last_sync_at': lastSyncAt,
|
||||
|
|
@ -522,6 +566,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
|
|||
Value<int>? xp,
|
||||
Value<int>? level,
|
||||
Value<double>? currentBodyweight,
|
||||
Value<Map<String, dynamic>?>? exerciseVariants,
|
||||
Value<Map<String, dynamic>?>? inventorySettings,
|
||||
Value<Map<String, dynamic>?>? avatarConfig,
|
||||
Value<DateTime?>? lastSyncAt,
|
||||
|
|
@ -535,6 +580,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
|
|||
xp: xp ?? this.xp,
|
||||
level: level ?? this.level,
|
||||
currentBodyweight: currentBodyweight ?? this.currentBodyweight,
|
||||
exerciseVariants: exerciseVariants ?? this.exerciseVariants,
|
||||
inventorySettings: inventorySettings ?? this.inventorySettings,
|
||||
avatarConfig: avatarConfig ?? this.avatarConfig,
|
||||
lastSyncAt: lastSyncAt ?? this.lastSyncAt,
|
||||
|
|
@ -565,6 +611,11 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
|
|||
if (currentBodyweight.present) {
|
||||
map['current_bodyweight'] = Variable<double>(currentBodyweight.value);
|
||||
}
|
||||
if (exerciseVariants.present) {
|
||||
map['exercise_variants'] = Variable<String>($UsersTable
|
||||
.$converterexerciseVariantsn
|
||||
.toSql(exerciseVariants.value));
|
||||
}
|
||||
if (inventorySettings.present) {
|
||||
map['inventory_settings'] = Variable<String>($UsersTable
|
||||
.$converterinventorySettingsn
|
||||
|
|
@ -598,6 +649,7 @@ class UsersCompanion extends UpdateCompanion<UserCollection> {
|
|||
..write('xp: $xp, ')
|
||||
..write('level: $level, ')
|
||||
..write('currentBodyweight: $currentBodyweight, ')
|
||||
..write('exerciseVariants: $exerciseVariants, ')
|
||||
..write('inventorySettings: $inventorySettings, ')
|
||||
..write('avatarConfig: $avatarConfig, ')
|
||||
..write('lastSyncAt: $lastSyncAt, ')
|
||||
|
|
@ -2452,6 +2504,7 @@ typedef $$UsersTableCreateCompanionBuilder = UsersCompanion Function({
|
|||
Value<int> xp,
|
||||
Value<int> level,
|
||||
Value<double> currentBodyweight,
|
||||
Value<Map<String, dynamic>?> exerciseVariants,
|
||||
Value<Map<String, dynamic>?> inventorySettings,
|
||||
Value<Map<String, dynamic>?> avatarConfig,
|
||||
Value<DateTime?> lastSyncAt,
|
||||
|
|
@ -2466,6 +2519,7 @@ typedef $$UsersTableUpdateCompanionBuilder = UsersCompanion Function({
|
|||
Value<int> xp,
|
||||
Value<int> level,
|
||||
Value<double> currentBodyweight,
|
||||
Value<Map<String, dynamic>?> exerciseVariants,
|
||||
Value<Map<String, dynamic>?> inventorySettings,
|
||||
Value<Map<String, dynamic>?> avatarConfig,
|
||||
Value<DateTime?> lastSyncAt,
|
||||
|
|
@ -2501,6 +2555,12 @@ class $$UsersTableFilterComposer extends Composer<_$AppDatabase, $UsersTable> {
|
|||
column: $table.currentBodyweight,
|
||||
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>,
|
||||
String>
|
||||
get inventorySettings => $composableBuilder(
|
||||
|
|
@ -2554,6 +2614,10 @@ class $$UsersTableOrderingComposer
|
|||
column: $table.currentBodyweight,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get exerciseVariants => $composableBuilder(
|
||||
column: $table.exerciseVariants,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get inventorySettings => $composableBuilder(
|
||||
column: $table.inventorySettings,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
|
@ -2602,6 +2666,10 @@ class $$UsersTableAnnotationComposer
|
|||
GeneratedColumn<double> get currentBodyweight => $composableBuilder(
|
||||
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>
|
||||
get inventorySettings => $composableBuilder(
|
||||
column: $table.inventorySettings, builder: (column) => column);
|
||||
|
|
@ -2655,6 +2723,8 @@ class $$UsersTableTableManager extends RootTableManager<
|
|||
Value<int> xp = const Value.absent(),
|
||||
Value<int> level = const Value.absent(),
|
||||
Value<double> currentBodyweight = const Value.absent(),
|
||||
Value<Map<String, dynamic>?> exerciseVariants =
|
||||
const Value.absent(),
|
||||
Value<Map<String, dynamic>?> inventorySettings =
|
||||
const Value.absent(),
|
||||
Value<Map<String, dynamic>?> avatarConfig = const Value.absent(),
|
||||
|
|
@ -2670,6 +2740,7 @@ class $$UsersTableTableManager extends RootTableManager<
|
|||
xp: xp,
|
||||
level: level,
|
||||
currentBodyweight: currentBodyweight,
|
||||
exerciseVariants: exerciseVariants,
|
||||
inventorySettings: inventorySettings,
|
||||
avatarConfig: avatarConfig,
|
||||
lastSyncAt: lastSyncAt,
|
||||
|
|
@ -2684,6 +2755,8 @@ class $$UsersTableTableManager extends RootTableManager<
|
|||
Value<int> xp = const Value.absent(),
|
||||
Value<int> level = const Value.absent(),
|
||||
Value<double> currentBodyweight = const Value.absent(),
|
||||
Value<Map<String, dynamic>?> exerciseVariants =
|
||||
const Value.absent(),
|
||||
Value<Map<String, dynamic>?> inventorySettings =
|
||||
const Value.absent(),
|
||||
Value<Map<String, dynamic>?> avatarConfig = const Value.absent(),
|
||||
|
|
@ -2699,6 +2772,7 @@ class $$UsersTableTableManager extends RootTableManager<
|
|||
xp: xp,
|
||||
level: level,
|
||||
currentBodyweight: currentBodyweight,
|
||||
exerciseVariants: exerciseVariants,
|
||||
inventorySettings: inventorySettings,
|
||||
avatarConfig: avatarConfig,
|
||||
lastSyncAt: lastSyncAt,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ class Users extends Table {
|
|||
IntColumn get level => integer().withDefault(const Constant(1))();
|
||||
RealColumn get currentBodyweight =>
|
||||
real().withDefault(const Constant(70.0))();
|
||||
TextColumn get exerciseVariants =>
|
||||
text().map(const MapConverter()).nullable()();
|
||||
|
||||
TextColumn get inventorySettings =>
|
||||
text().map(const MapConverter()).nullable()();
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ class ApiClient {
|
|||
required String password,
|
||||
required double bodyweight,
|
||||
required Map<String, dynamic> inventorySettings,
|
||||
Map<String, dynamic>? exerciseVariants,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
|
|
@ -97,6 +98,7 @@ class ApiClient {
|
|||
'level': 1,
|
||||
'current_bodyweight': bodyweight,
|
||||
'inventory_settings': inventorySettings,
|
||||
'exercise_variants': exerciseVariants ?? {},
|
||||
'avatar_config': {
|
||||
'skin_tone': 'medium',
|
||||
'hair_style': 'short_01',
|
||||
|
|
|
|||
|
|
@ -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 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
|
@ -261,7 +31,6 @@ class SyncService {
|
|||
try {
|
||||
debugPrint('🔄 Starting Sync...');
|
||||
|
||||
// 1. CYCLES SYNC
|
||||
final dirtyCycles = await (db.select(db.cycles)
|
||||
..where((c) => c.isDirty.equals(true)))
|
||||
.get();
|
||||
|
|
@ -285,7 +54,6 @@ class SyncService {
|
|||
),
|
||||
);
|
||||
|
||||
// Relink workouts
|
||||
final oldLocalIdRef = cycle.id.toString();
|
||||
await (db.update(db.workouts)
|
||||
..where((w) => w.cycleId.equals(oldLocalIdRef)))
|
||||
|
|
@ -305,7 +73,6 @@ class SyncService {
|
|||
}
|
||||
}
|
||||
|
||||
// 2. USER & WORKOUTS SYNC
|
||||
final dirtyUser = await (db.select(db.users)
|
||||
..where((u) => u.isDirty.equals(true)))
|
||||
.getSingleOrNull();
|
||||
|
|
@ -335,6 +102,7 @@ class SyncService {
|
|||
'xp': dirtyUser.xp,
|
||||
'level': dirtyUser.level,
|
||||
'current_bodyweight': dirtyUser.currentBodyweight,
|
||||
'exercise_variants': dirtyUser.exerciseVariants,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
|
@ -350,7 +118,6 @@ class SyncService {
|
|||
);
|
||||
|
||||
await db.transaction(() async {
|
||||
// Clean dirty flags
|
||||
if (dirtyUser != null) {
|
||||
await (db.update(db.users)..where((u) => u.id.equals(dirtyUser.id)))
|
||||
.write(const UsersCompanion(isDirty: Value(false)));
|
||||
|
|
@ -360,9 +127,7 @@ class SyncService {
|
|||
.write(const WorkoutsCompanion(isDirty: Value(false)));
|
||||
}
|
||||
|
||||
// PROCESS PULL DATA
|
||||
if (response['pull_data'] != null) {
|
||||
// Cycles Pull
|
||||
if (response['pull_data']['cycles'] != null) {
|
||||
final pulledCycles = response['pull_data']['cycles'] as List;
|
||||
for (var cJson in pulledCycles) {
|
||||
|
|
@ -397,7 +162,6 @@ class SyncService {
|
|||
}
|
||||
}
|
||||
|
||||
// Workouts Pull - MIT DUPLIKAT-SCHUTZ
|
||||
if (response['pull_data']['workouts'] != null) {
|
||||
final pulledWorkouts = response['pull_data']['workouts'] as List;
|
||||
debugPrint('📥 Pulled ${pulledWorkouts.length} workouts.');
|
||||
|
|
@ -408,13 +172,10 @@ class SyncService {
|
|||
final week = wJson['week'] as int;
|
||||
final day = wJson['day'] as int;
|
||||
|
||||
// 1. Versuch: Match über Server ID
|
||||
var existing = await (db.select(db.workouts)
|
||||
..where((w) => w.serverId.equals(serverId)))
|
||||
.getSingleOrNull();
|
||||
|
||||
// 2. Versuch: Match über Logik (Cycle + Week + Day)
|
||||
// Das verhindert Duplikate, wenn ServerID lokal noch fehlt
|
||||
if (existing == null) {
|
||||
final candidates = await (db.select(db.workouts)
|
||||
..where((w) =>
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ class UserRepository {
|
|||
required String password,
|
||||
required double bodyweight,
|
||||
required Map<String, dynamic> inventorySettings,
|
||||
Map<String, dynamic>? exerciseVariants,
|
||||
}) async {
|
||||
try {
|
||||
final response = await apiClient.register(
|
||||
|
|
@ -112,10 +113,27 @@ class UserRepository {
|
|||
password: password,
|
||||
bodyweight: bodyweight,
|
||||
inventorySettings: inventorySettings,
|
||||
exerciseVariants: exerciseVariants,
|
||||
);
|
||||
|
||||
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 {
|
||||
await apiClient.login(email, password);
|
||||
|
|
@ -138,6 +156,7 @@ class UserRepository {
|
|||
currentBodyweight:
|
||||
Value((record['current_bodyweight'] as num?)?.toDouble() ?? 70.0),
|
||||
inventorySettings: Value(record['inventory_settings'] ?? {}),
|
||||
exerciseVariants: Value(record['exercise_variants'] ?? {}),
|
||||
avatarConfig: Value(record['avatar_config'] ?? {}),
|
||||
lastSyncAt: Value(DateTime.now()),
|
||||
isDirty: const Value(false),
|
||||
|
|
@ -159,6 +178,7 @@ class UserRepository {
|
|||
await db.delete(db.users).go();
|
||||
await db.delete(db.cycles).go();
|
||||
await db.delete(db.workouts).go();
|
||||
await db.delete(db.quests).go();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'dart:math';
|
|||
import '../entities/workout_set.dart';
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
|
||||
enum ExerciseType { squat, pullup, dip }
|
||||
enum ExerciseType { squat, pullup, dip, row, bench }
|
||||
|
||||
class WendlerCalculator {
|
||||
static const Map<int, List<double>> weekPercentages = {
|
||||
|
|
@ -51,8 +51,38 @@ class WendlerCalculator {
|
|||
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) {
|
||||
final step = type == ExerciseType.squat
|
||||
final step = (type == ExerciseType.squat ||
|
||||
type == ExerciseType.row ||
|
||||
type == ExerciseType.bench)
|
||||
? AppConstants.squatRoundingStep
|
||||
: AppConstants.calisthenicsRoundingStep;
|
||||
return (weight / step).floor() * step;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue