refactor: clean up smaller fixes
This commit is contained in:
parent
2609446e9a
commit
32e935b3ec
33 changed files with 1292 additions and 1389 deletions
BIN
assets/audio/beep_long.ogg
Normal file
BIN
assets/audio/beep_long.ogg
Normal file
Binary file not shown.
BIN
assets/audio/beep_short.ogg
Normal file
BIN
assets/audio/beep_short.ogg
Normal file
Binary file not shown.
|
|
@ -13,14 +13,14 @@ class AppConstants {
|
|||
|
||||
// XP System
|
||||
static const int baseXP = 1000;
|
||||
static const double xpMultiplier = 1.15;
|
||||
static const double xpMultiplier = 1.25;
|
||||
static const int maxLevel = 100;
|
||||
|
||||
// XP Rewards
|
||||
static const int workoutCompleteXP = 100;
|
||||
static const double volumeXPRate = 0.1; // XP per kg
|
||||
static const double volumeXPRate = 0.01; // XP per kg
|
||||
static const int amrapBonusXPPerRep = 25;
|
||||
static const int prBonusXP = 500;
|
||||
static const int prBonusXP = 200;
|
||||
static const int cycleCompleteXP = 500;
|
||||
|
||||
// Rounding Steps
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ class AssetPaths {
|
|||
static String getAvatarPath(String gender, int variant) {
|
||||
return 'assets/images/avatars/$gender/$variant.png';
|
||||
}
|
||||
|
||||
static const String audioBeepShort = 'audio/beep_short.ogg';
|
||||
static const String audioBeepLong = 'audio/beep_long.ogg';
|
||||
}
|
||||
|
||||
class PlateColors {
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
|
|||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
|
|
@ -176,11 +176,11 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
|
|||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00E5FF).withOpacity(0.9),
|
||||
color: const Color(0xFF00E5FF).withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF00E5FF).withOpacity(0.6),
|
||||
color: const Color(0xFF00E5FF).withValues(alpha: 0.6),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class AppTheme {
|
|||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(
|
||||
color: primaryColor.withOpacity(0.3),
|
||||
color: primaryColor.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
|
|
@ -97,11 +97,11 @@ class AppTheme {
|
|||
fillColor: surfaceColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: primaryColor.withOpacity(0.5)),
|
||||
borderSide: BorderSide(color: primaryColor.withValues(alpha: 0.5)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: primaryColor.withOpacity(0.3)),
|
||||
borderSide: BorderSide(color: primaryColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import '../../../gamification/domain/entities/avatar_config.dart';
|
|||
import '../../../gamification/presentation/widgets/avatar_editor.dart';
|
||||
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
|
||||
import '../../../gamification/domain/entities/item_catalog.dart';
|
||||
import '../../../../shared/domain/logic/wendler_calculator.dart';
|
||||
|
||||
class ProfileScreen extends ConsumerStatefulWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
|
@ -176,11 +177,10 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
Navigator.pop(context);
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// Update Config
|
||||
final newConfig = AvatarConfig(
|
||||
gender: currentConfig.gender,
|
||||
variant: currentConfig.variant,
|
||||
selectedBackground: item.id, // Hintergrund setzen
|
||||
selectedBackground: item.id,
|
||||
);
|
||||
|
||||
final updatedUser = _user!.copyWith(
|
||||
|
|
@ -193,12 +193,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
.read(userRepositoryProvider)
|
||||
.saveLocalUser(_user!);
|
||||
setState(() => _isLoading = false);
|
||||
// Save to DB
|
||||
// await ref
|
||||
// .read(userRepositoryProvider)
|
||||
// .updateAvatarConfig(newConfig.toJson());
|
||||
// await _loadUser();
|
||||
// setState(() => _isLoading = false);
|
||||
}
|
||||
: null,
|
||||
child: Container(
|
||||
|
|
@ -256,7 +250,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
bottom: Radius.circular(10)),
|
||||
),
|
||||
|
|
@ -358,6 +352,48 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
AccessoryTemplate _getTemplateFromSettings(Map<String, dynamic> settings) {
|
||||
final key = settings['accessory_template'] as String?;
|
||||
if (key == 'hypertrophy') return AccessoryTemplate.hypertrophy;
|
||||
if (key == 'conditioning') return AccessoryTemplate.conditioning;
|
||||
return AccessoryTemplate.none;
|
||||
}
|
||||
|
||||
Future<void> _updateTemplate(AccessoryTemplate newTemplate) async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
String templateKey = 'none';
|
||||
if (newTemplate == AccessoryTemplate.hypertrophy) {
|
||||
templateKey = 'hypertrophy';
|
||||
}
|
||||
if (newTemplate == AccessoryTemplate.conditioning) {
|
||||
templateKey = 'conditioning';
|
||||
}
|
||||
|
||||
final currentSettings =
|
||||
Map<String, dynamic>.from(_user!.inventorySettings ?? {});
|
||||
currentSettings['accessory_template'] = templateKey;
|
||||
|
||||
try {
|
||||
final updatedUser = _user!.copyWith(
|
||||
inventorySettings: Value(currentSettings),
|
||||
isDirty: true,
|
||||
);
|
||||
|
||||
await ref.read(userRepositoryProvider).saveLocalUser(updatedUser);
|
||||
|
||||
ref.read(userRepositoryProvider).updateInventory(currentSettings);
|
||||
|
||||
setState(() {
|
||||
_user = updatedUser;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => _isLoading = false);
|
||||
// Error handling...
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userRepo = ref.watch(userRepositoryProvider);
|
||||
|
|
@ -411,7 +447,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
// const SizedBox(height: 16),
|
||||
Center(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _showBackgroundSelector,
|
||||
|
|
@ -441,7 +476,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
value: _currentBodyweight,
|
||||
min: 40,
|
||||
max: 150,
|
||||
divisions: 220, // 0.5 steps
|
||||
divisions: 220,
|
||||
label: _currentBodyweight.toStringAsFixed(1),
|
||||
activeColor: AppTheme.primaryColor,
|
||||
onChanged: (val) => setState(() {
|
||||
|
|
@ -467,6 +502,26 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text('Training Focus',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(color: AppTheme.textPrimary)),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Accessory Template',
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
const SizedBox(height: 12),
|
||||
_buildTemplateSelector(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('Account Security',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
|
|
@ -489,10 +544,10 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: AppTheme.errorColor.withOpacity(0.5)),
|
||||
border: Border.all(
|
||||
color: AppTheme.errorColor.withValues(alpha: 0.5)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: AppTheme.errorColor.withOpacity(0.05),
|
||||
color: AppTheme.errorColor.withValues(alpha: 0.05),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
|
|
@ -562,4 +617,72 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTemplateSelector() {
|
||||
final current = _getTemplateFromSettings(_user?.inventorySettings ?? {});
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_RadioTile<AccessoryTemplate>(
|
||||
value: AccessoryTemplate.none,
|
||||
groupValue: current,
|
||||
title: 'Strength Only',
|
||||
subtitle: 'Main Lifts + FSL. Pure & Fast.',
|
||||
onChanged: (val) => _updateTemplate(val!),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_RadioTile<AccessoryTemplate>(
|
||||
value: AccessoryTemplate.hypertrophy,
|
||||
groupValue: current,
|
||||
title: 'Hypertrophy Support',
|
||||
subtitle: 'Bodybuilding accessories to build muscle armor.',
|
||||
onChanged: (val) => _updateTemplate(val!),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_RadioTile<AccessoryTemplate>(
|
||||
value: AccessoryTemplate.conditioning,
|
||||
groupValue: current,
|
||||
title: 'The Engine (Conditioning)',
|
||||
subtitle: '15 min Kettlebell intervals to boost stamina.',
|
||||
onChanged: (val) => _updateTemplate(val!),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RadioTile<T> extends StatelessWidget {
|
||||
final T value;
|
||||
final T groupValue;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final ValueChanged<T?> onChanged;
|
||||
|
||||
const _RadioTile({
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSelected = value == groupValue;
|
||||
return RadioListTile<T>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
activeColor: AppTheme.primaryColor,
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected ? AppTheme.primaryColor : Colors.white,
|
||||
),
|
||||
),
|
||||
subtitle: Text(subtitle, style: const TextStyle(fontSize: 12)),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
// 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/theme/app_theme.dart';
|
||||
import '../../../../shared/data/local/app_database.dart';
|
||||
import '../../../../shared/data/repositories/user_repository.dart';
|
||||
|
|
@ -23,6 +20,7 @@ import '../widgets/xp_bar_widget.dart';
|
|||
import '../widgets/level_display.dart';
|
||||
import '../widgets/start_raid_button.dart';
|
||||
import '../../../gamification/application/quest_service.dart';
|
||||
import '../../../workout_runner/application/workout_generator_service.dart';
|
||||
|
||||
class HubScreen extends ConsumerStatefulWidget {
|
||||
const HubScreen({super.key});
|
||||
|
|
@ -49,102 +47,11 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
List<Exercise> _generateExercises({
|
||||
required int week,
|
||||
required int day,
|
||||
required Map<String, double> trainingMaxes,
|
||||
required double bodyweight,
|
||||
required UserCollection user,
|
||||
}) {
|
||||
final exercises = <Exercise>[];
|
||||
final variants = user.exerciseVariants ?? {};
|
||||
|
||||
(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) {
|
||||
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 (type == ExerciseType.row || type == ExerciseType.bench) return;
|
||||
|
||||
sets = WendlerCalculator.generateFSLSets(
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: user.currentBodyweight,
|
||||
);
|
||||
}
|
||||
|
||||
if (sets.isNotEmpty) {
|
||||
exercises.add(Exercise(
|
||||
exerciseId: id,
|
||||
exerciseName: isMain ? name : '$name (FSL)',
|
||||
bodyweightAtSession: user.currentBodyweight,
|
||||
sets: sets,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (day == 1) {
|
||||
addExercise('legs', 'squat', 'Back Squat', ExerciseType.squat, true);
|
||||
addExercise(
|
||||
'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, false);
|
||||
} else if (day == 2) {
|
||||
addExercise('push', 'dip', 'Weighted Dip', ExerciseType.dip, true);
|
||||
addExercise('legs', 'squat', 'Back Squat', ExerciseType.squat, false);
|
||||
} else if (day == 3) {
|
||||
addExercise(
|
||||
'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, true);
|
||||
addExercise('push', 'dip', 'Weighted Dip', ExerciseType.dip, false);
|
||||
}
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
Future<void> _startNextWorkout(
|
||||
CycleCollection cycle, UserCollection user) async {
|
||||
try {
|
||||
final workoutRepo = ref.read(workoutRepositoryProvider);
|
||||
final workoutGenerator = ref.read(workoutGeneratorServiceProvider);
|
||||
|
||||
final tmsDynamic = cycle.trainingMaxes;
|
||||
final trainingMaxes = Map<String, double>.from(tmsDynamic
|
||||
|
|
@ -181,7 +88,6 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var workout = await workoutRepo.getWorkoutByWeekDay(
|
||||
cycleId: cycleRefId,
|
||||
localCycleId: localCycleId,
|
||||
|
|
@ -190,12 +96,22 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
);
|
||||
|
||||
if (workout == null) {
|
||||
final exercises = _generateExercises(
|
||||
week: targetWeek,
|
||||
day: targetDay,
|
||||
trainingMaxes: trainingMaxes,
|
||||
bodyweight: user.currentBodyweight,
|
||||
user: user);
|
||||
final activeTemplate = _getTemplateFromUser(user);
|
||||
int? conditioningSets;
|
||||
|
||||
if (activeTemplate == AccessoryTemplate.conditioning) {
|
||||
conditioningSets = await _showConditioningDialog();
|
||||
if (conditioningSets == null) return;
|
||||
}
|
||||
|
||||
final exercises = workoutGenerator.generateWorkout(
|
||||
week: targetWeek,
|
||||
day: targetDay,
|
||||
trainingMaxes: trainingMaxes,
|
||||
user: user,
|
||||
template: activeTemplate,
|
||||
conditioningSets: conditioningSets,
|
||||
);
|
||||
|
||||
final userId = user.serverId ?? user.id.toString();
|
||||
|
||||
|
|
@ -225,6 +141,88 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
AccessoryTemplate _getTemplateFromUser(UserCollection user) {
|
||||
final settings = user.inventorySettings ?? {};
|
||||
final key = settings['accessory_template'] as String?;
|
||||
if (key == 'hypertrophy') return AccessoryTemplate.hypertrophy;
|
||||
if (key == 'conditioning') return AccessoryTemplate.conditioning;
|
||||
return AccessoryTemplate.none;
|
||||
}
|
||||
|
||||
Future<int?> _showConditioningDialog() async {
|
||||
int sets = 20;
|
||||
|
||||
return await showDialog<int>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
final interval = (20 * 60) / sets;
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text(
|
||||
'MISSION BRIEFING',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'The enemy is fleeing! We have a 20-minute window to intercept.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Combat Density: $sets Sets',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
'Interval: Every ${interval.toStringAsFixed(0)} seconds',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Slider(
|
||||
value: sets.toDouble(),
|
||||
min: 10,
|
||||
max: 30,
|
||||
divisions: 20,
|
||||
activeColor: AppTheme.primaryColor,
|
||||
onChanged: (val) {
|
||||
setDialogState(() => sets = val.toInt());
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (sets >= 20)
|
||||
const Text('⚠️ HARDCORE MODE',
|
||||
style: TextStyle(
|
||||
color: AppTheme.errorColor,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, null),
|
||||
child: const Text('ABORT'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, sets),
|
||||
child: const Text('ENGAGE'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userRepo = ref.watch(userRepositoryProvider);
|
||||
|
|
@ -280,9 +278,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
// Colors.black.withOpacity(0.6),
|
||||
Colors.black.withValues(alpha: 0.6),
|
||||
// Colors.black.withOpacity(0.85),
|
||||
Colors.black.withValues(alpha: 0.85),
|
||||
],
|
||||
),
|
||||
|
|
@ -389,7 +385,6 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
top: Radius.circular(24)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
// color: Colors.black.withOpacity(0.2),
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -5),
|
||||
|
|
@ -435,10 +430,6 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
// extension on Object {
|
||||
// operator [](String other) {}
|
||||
// }
|
||||
|
||||
class _StatBox extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
|
@ -456,7 +447,6 @@ class _StatBox extends StatelessWidget {
|
|||
color: AppTheme.surfaceColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
// color: AppTheme.primaryColor.withOpacity(0.3),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class LevelDisplay extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.primaryColor.withOpacity(0.5),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.5),
|
||||
blurRadius: 12,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
|
|
@ -57,4 +57,3 @@ class LevelDisplay extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ class _StartRaidButtonState extends State<StartRaidButton>
|
|||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.primaryColor.withOpacity(_glowAnimation.value),
|
||||
color: AppTheme.primaryColor
|
||||
.withValues(alpha: _glowAnimation.value),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5,
|
||||
),
|
||||
|
|
@ -96,4 +97,3 @@ class _StartRaidButtonState extends State<StartRaidButton>
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class XPBarWidget extends StatelessWidget {
|
|||
color: AppTheme.xpBarBackground,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppTheme.primaryColor.withOpacity(0.3),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.3),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
|
|
@ -64,13 +64,13 @@ class XPBarWidget extends StatelessWidget {
|
|||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppTheme.primaryColor,
|
||||
AppTheme.primaryColor.withOpacity(0.7),
|
||||
AppTheme.primaryColor.withValues(alpha: 0.7),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.primaryColor.withOpacity(0.5),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.5),
|
||||
blurRadius: 8,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
|
|
@ -86,15 +86,15 @@ class XPBarWidget extends StatelessWidget {
|
|||
child: Text(
|
||||
'${(progress * 100).toStringAsFixed(0)}%',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
const Shadow(
|
||||
color: Colors.black,
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
const Shadow(
|
||||
color: Colors.black,
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -103,4 +103,3 @@ class XPBarWidget extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,14 +83,14 @@ class _LoreCard extends StatelessWidget {
|
|||
clipBehavior: Clip.antiAlias,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: color.withOpacity(0.5), width: 1),
|
||||
border: Border.all(color: color.withValues(alpha: 0.5), width: 1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
AppTheme.surfaceColor,
|
||||
color.withOpacity(0.1),
|
||||
color.withValues(alpha: 0.1),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -112,7 +112,7 @@ class _LoreCard extends StatelessWidget {
|
|||
child: Image.asset(
|
||||
assetPath,
|
||||
fit: BoxFit.contain,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
colorBlendMode: BlendMode.modulate,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class AvatarRenderer extends StatelessWidget {
|
|||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../shared/data/local/app_database.dart'; // Für QuestCollection Klasse
|
||||
import '../../../../shared/data/local/app_database.dart';
|
||||
import '../../data/repositories/quest_repository.dart';
|
||||
import '../../domain/entities/item_catalog.dart';
|
||||
|
||||
|
|
@ -23,19 +23,9 @@ class _QuestItemState extends ConsumerState<QuestItem> {
|
|||
Future<void> _handleClaim() async {
|
||||
setState(() => _isClaiming = true);
|
||||
try {
|
||||
// 1. XP und Item gutschreiben (Logik im Repo oder Service wäre besser,
|
||||
// aber für MVP machen wir den Claim im Repo und User-Update hier oder im Service).
|
||||
// Einfachheitshalber: Repo setzt isClaimed=true. Wir müssen aber auch XP geben.
|
||||
// BESSER: Wir nutzen einen QuestService Methode 'claimReward', die beides macht.
|
||||
// Da wir die noch nicht haben, machen wir es hier "manuell" via Repos.
|
||||
|
||||
final questRepo = ref.read(questRepositoryProvider);
|
||||
await questRepo.claimQuest(widget.quest.id);
|
||||
|
||||
// Wir verlassen uns darauf, dass der UserRepo/XP Service das separat regelt
|
||||
// oder wir feuern hier ein Event.
|
||||
// Für das UI Feedback reicht erst mal das Claimen.
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
|
|
@ -52,7 +42,8 @@ class _QuestItemState extends ConsumerState<QuestItem> {
|
|||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isClaiming = false);
|
||||
|
|
@ -61,7 +52,8 @@ class _QuestItemState extends ConsumerState<QuestItem> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final progress = (widget.quest.currentValue / widget.quest.targetValue).clamp(0.0, 1.0);
|
||||
final progress =
|
||||
(widget.quest.currentValue / widget.quest.targetValue).clamp(0.0, 1.0);
|
||||
final isComplete = widget.quest.isCompleted;
|
||||
final isClaimed = widget.quest.isClaimed;
|
||||
|
||||
|
|
@ -83,7 +75,9 @@ class _QuestItemState extends ConsumerState<QuestItem> {
|
|||
children: [
|
||||
Icon(
|
||||
_getIconForType(widget.quest.type),
|
||||
color: isComplete ? AppTheme.successColor : AppTheme.primaryColor,
|
||||
color: isComplete
|
||||
? AppTheme.successColor
|
||||
: AppTheme.primaryColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
|
@ -91,28 +85,29 @@ class _QuestItemState extends ConsumerState<QuestItem> {
|
|||
child: Text(
|
||||
widget.quest.title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isClaimed ? Colors.grey : Colors.white,
|
||||
decoration: isClaimed ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isClaimed ? Colors.grey : Colors.white,
|
||||
decoration:
|
||||
isClaimed ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isClaimed)
|
||||
const Icon(Icons.check, color: Colors.grey, size: 20)
|
||||
else if (widget.quest.rewardXP > 0)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.xpBarFill.withOpacity(0.2),
|
||||
color: AppTheme.xpBarFill.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'+${widget.quest.rewardXP} XP',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
color: AppTheme.primaryColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -121,7 +116,9 @@ class _QuestItemState extends ConsumerState<QuestItem> {
|
|||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.quest.description,
|
||||
style: TextStyle(color: isClaimed ? Colors.grey : AppTheme.textSecondary, fontSize: 12),
|
||||
style: TextStyle(
|
||||
color: isClaimed ? Colors.grey : AppTheme.textSecondary,
|
||||
fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
|
@ -137,34 +134,42 @@ class _QuestItemState extends ConsumerState<QuestItem> {
|
|||
child: LinearProgressIndicator(
|
||||
value: progress,
|
||||
backgroundColor: Colors.grey[800],
|
||||
color: isComplete ? AppTheme.successColor : AppTheme.primaryColor,
|
||||
color: isComplete
|
||||
? AppTheme.successColor
|
||||
: AppTheme.primaryColor,
|
||||
minHeight: 8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${widget.quest.currentValue} / ${widget.quest.targetValue}',
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 10),
|
||||
style:
|
||||
const TextStyle(color: Colors.grey, fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
if (isComplete && !isClaimed)
|
||||
ElevatedButton(
|
||||
onPressed: _isClaiming ? null : _handleClaim,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.successColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 0),
|
||||
minimumSize: const Size(0, 32),
|
||||
),
|
||||
child: _isClaiming
|
||||
? const SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Text('CLAIM', style: TextStyle(fontSize: 12)),
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Text('CLAIM', style: TextStyle(fontSize: 12)),
|
||||
)
|
||||
else if (widget.quest.rewardItem != null && !isClaimed)
|
||||
const Icon(Icons.inventory_2, color: AppTheme.secondaryColor, size: 20),
|
||||
const Icon(Icons.inventory_2,
|
||||
color: AppTheme.secondaryColor, size: 20),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
@ -175,10 +180,14 @@ class _QuestItemState extends ConsumerState<QuestItem> {
|
|||
|
||||
IconData _getIconForType(String type) {
|
||||
switch (type) {
|
||||
case 'daily': return Icons.today;
|
||||
case 'story': return Icons.auto_stories;
|
||||
case 'milestone': return Icons.emoji_events;
|
||||
default: return Icons.task_alt;
|
||||
case 'daily':
|
||||
return Icons.today;
|
||||
case 'story':
|
||||
return Icons.auto_stories;
|
||||
case 'milestone':
|
||||
return Icons.emoji_events;
|
||||
default:
|
||||
return Icons.task_alt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
|||
Icon(
|
||||
Icons.history_edu,
|
||||
size: 80,
|
||||
color: AppTheme.primaryColor.withOpacity(0.5),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
|
|
@ -180,9 +180,9 @@ class _WorkoutHistoryCard extends StatelessWidget {
|
|||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.primaryColor.withOpacity(0.3)),
|
||||
border: Border.all(color: AppTheme.primaryColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
|
|||
|
|
@ -127,7 +127,9 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
|||
|
||||
final platesList = <double>[];
|
||||
_plateInventory.forEach((weight, count) {
|
||||
for (int i = 0; i < count; i++) platesList.add(weight);
|
||||
for (int i = 0; i < count; i++) {
|
||||
platesList.add(weight);
|
||||
}
|
||||
});
|
||||
|
||||
final bandsList = <Map<String, dynamic>>[];
|
||||
|
|
@ -258,7 +260,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
|||
?.copyWith(color: AppTheme.textSecondary)),
|
||||
const SizedBox(height: 8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Row(
|
||||
children: [
|
||||
ActionChip(
|
||||
|
|
@ -315,7 +317,8 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
|||
_hasChanges = true;
|
||||
});
|
||||
},
|
||||
selectedColor: _getBandColor(entry.key).withOpacity(0.3),
|
||||
selectedColor:
|
||||
_getBandColor(entry.key).withValues(alpha: 0.3),
|
||||
checkmarkColor: _getBandColor(entry.key),
|
||||
side: BorderSide(color: _getBandColor(entry.key)),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -68,10 +68,10 @@ class PlateCounter extends StatelessWidget {
|
|||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border:
|
||||
Border.all(color: AppTheme.primaryColor.withOpacity(0.3)),
|
||||
border: Border.all(
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Text(
|
||||
count.toString(),
|
||||
|
|
|
|||
|
|
@ -353,7 +353,8 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
|
|||
_bandInventory[entry.key] = selected;
|
||||
});
|
||||
},
|
||||
selectedColor: _getBandColor(entry.key).withOpacity(0.3),
|
||||
selectedColor:
|
||||
_getBandColor(entry.key).withValues(alpha: 0.3),
|
||||
checkmarkColor: _getBandColor(entry.key),
|
||||
labelStyle: TextStyle(
|
||||
color: entry.value ? Colors.white : Colors.grey,
|
||||
|
|
|
|||
|
|
@ -212,10 +212,10 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppTheme.primaryColor.withOpacity(0.3)),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
@ -281,7 +281,7 @@ class _ExerciseCard extends StatelessWidget {
|
|||
children: [
|
||||
Text(title.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
|
|
@ -290,7 +290,7 @@ class _ExerciseCard extends StatelessWidget {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.2),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: Icon(icon, color: AppTheme.primaryColor),
|
||||
),
|
||||
|
|
@ -390,7 +390,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
|
|||
children: [
|
||||
Text(slotTitle.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold)),
|
||||
Row(
|
||||
|
|
@ -403,7 +403,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
|
|||
: Colors.grey)),
|
||||
Switch(
|
||||
value: isCapable,
|
||||
activeColor: AppTheme.successColor,
|
||||
activeThumbColor: AppTheme.successColor,
|
||||
onChanged: onToggleCapability,
|
||||
),
|
||||
],
|
||||
|
|
@ -416,7 +416,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.2),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: Icon(icon, color: AppTheme.primaryColor),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class WelcomeScreen extends StatelessWidget {
|
|||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
|
|
@ -35,11 +35,11 @@ class WelcomeScreen extends StatelessWidget {
|
|||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.9),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.9),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.primaryColor.withOpacity(0.5),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.5),
|
||||
blurRadius: 20)
|
||||
],
|
||||
),
|
||||
|
|
@ -139,7 +139,7 @@ class _FeatureItem extends StatelessWidget {
|
|||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.2),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ class _CurrentCycleCard extends StatelessWidget {
|
|||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.successColor.withOpacity(0.2),
|
||||
color: AppTheme.successColor.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
|
|
@ -502,14 +502,15 @@ class _FilterChip extends StatelessWidget {
|
|||
label: Text(label),
|
||||
selected: isSelected,
|
||||
onSelected: (_) => onTap(),
|
||||
selectedColor: AppTheme.primaryColor.withOpacity(0.2),
|
||||
selectedColor: AppTheme.primaryColor.withValues(alpha: 0.2),
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? AppTheme.primaryColor : Colors.grey,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
side: BorderSide(
|
||||
color:
|
||||
isSelected ? AppTheme.primaryColor : Colors.grey.withOpacity(0.3),
|
||||
color: isSelected
|
||||
? AppTheme.primaryColor
|
||||
: Colors.grey.withValues(alpha: 0.3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class ProgressChart extends StatelessWidget {
|
|||
decoration: BoxDecoration(
|
||||
color: AppTheme.surfaceColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppTheme.primaryColor.withOpacity(0.1)),
|
||||
border: Border.all(color: AppTheme.primaryColor.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
|
@ -148,14 +148,12 @@ class ProgressChart extends StatelessWidget {
|
|||
),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
lineTouchData: LineTouchData(
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
// FIX 2: Alte API nutzen (tooltipBgColor statt getTooltipColor)
|
||||
// tooltipBgColor: AppTheme.surfaceColor,
|
||||
getTooltipColor: (touchedSpot) => AppTheme.surfaceColor,
|
||||
getTooltipItems: (touchedSpots) {
|
||||
return touchedSpots.map((spot) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,256 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.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';
|
||||
|
||||
final workoutGeneratorServiceProvider =
|
||||
Provider<WorkoutGeneratorService>((ref) {
|
||||
return WorkoutGeneratorService();
|
||||
});
|
||||
|
||||
class WorkoutGeneratorService {
|
||||
List<Exercise> generateWorkout({
|
||||
required int week,
|
||||
required int day,
|
||||
required Map<String, double> trainingMaxes,
|
||||
required UserCollection user,
|
||||
required AccessoryTemplate template,
|
||||
int? conditioningSets,
|
||||
}) {
|
||||
final exercises = <Exercise>[];
|
||||
|
||||
exercises.addAll(_generateMainLifts(week, day, trainingMaxes, user));
|
||||
|
||||
if (template == AccessoryTemplate.hypertrophy) {
|
||||
exercises
|
||||
.addAll(_generateHypertrophyAccessories(day, trainingMaxes, user));
|
||||
} else if (template == AccessoryTemplate.conditioning) {
|
||||
final sets = (conditioningSets != null && conditioningSets > 0)
|
||||
? conditioningSets
|
||||
: 15;
|
||||
exercises.addAll(_generateConditioning(day, sets));
|
||||
}
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
List<Exercise> _generateMainLifts(int week, int day,
|
||||
Map<String, double> trainingMaxes, UserCollection user) {
|
||||
final exercises = <Exercise>[];
|
||||
final variants = user.exerciseVariants ?? {};
|
||||
|
||||
(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 tm = trainingMaxes[defaultId] ?? 0.0;
|
||||
List<WorkoutSet> sets;
|
||||
|
||||
if (isMain) {
|
||||
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 (type == ExerciseType.row || type == ExerciseType.bench) return;
|
||||
|
||||
sets = WendlerCalculator.generateFSLSets(
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: user.currentBodyweight,
|
||||
);
|
||||
}
|
||||
|
||||
if (sets.isNotEmpty) {
|
||||
exercises.add(Exercise(
|
||||
exerciseId: id,
|
||||
exerciseName: isMain ? name : '$name (FSL)',
|
||||
bodyweightAtSession: user.currentBodyweight,
|
||||
sets: sets,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (day == 1) {
|
||||
addExercise('legs', 'squat', 'Back Squat', ExerciseType.squat, true);
|
||||
addExercise(
|
||||
'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, false);
|
||||
} else if (day == 2) {
|
||||
addExercise('push', 'dip', 'Weighted Dip', ExerciseType.dip, true);
|
||||
addExercise('legs', 'squat', 'Back Squat', ExerciseType.squat, false);
|
||||
} else if (day == 3) {
|
||||
addExercise(
|
||||
'pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, true);
|
||||
addExercise('push', 'dip', 'Weighted Dip', ExerciseType.dip, false);
|
||||
}
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
List<Exercise> _generateHypertrophyAccessories(
|
||||
int day, Map<String, double> trainingMaxes, UserCollection user) {
|
||||
final accessories = <Exercise>[];
|
||||
|
||||
double calculateWeight(double referenceTm, double percentage) {
|
||||
final raw = referenceTm * percentage;
|
||||
return (raw / 2.5).round() * 2.5;
|
||||
}
|
||||
|
||||
Exercise createSimple(String id, String name, int sets, int reps,
|
||||
{double weight = 0.0}) {
|
||||
return Exercise(
|
||||
exerciseId: id,
|
||||
exerciseName: name,
|
||||
bodyweightAtSession: 0,
|
||||
sets: List.generate(
|
||||
sets,
|
||||
(i) => WorkoutSet(
|
||||
setNumber: i + 1,
|
||||
repsTarget: reps,
|
||||
targetWeightTotal: weight,
|
||||
repsActual: 0,
|
||||
isAmrap: false,
|
||||
completed: false,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
final squatTm = trainingMaxes['squat'] ?? 0.0;
|
||||
final dipTm = trainingMaxes['dip'] ?? 0.0;
|
||||
final pullupTm = trainingMaxes['pullup'] ?? 0.0;
|
||||
|
||||
switch (day) {
|
||||
case 1: // Squat Tag
|
||||
// RDL: ~40% vom Squat TM
|
||||
accessories.add(createSimple('rdl', 'Romanian Deadlift', 3, 10,
|
||||
weight: calculateWeight(squatTm, 0.4)));
|
||||
|
||||
accessories.add(_createIntervalExercise(
|
||||
id: 'kb_swing',
|
||||
name: '2H KB Swing',
|
||||
sets: 10,
|
||||
intervalSeconds: 60,
|
||||
repsPerSet: 10));
|
||||
break;
|
||||
|
||||
case 2: // Dip Tag (Push)
|
||||
// OHP: ~30% vom System-Dip-TM (konservativ für 3x10)
|
||||
accessories.add(createSimple('ohp', 'Overhead Press', 3, 10,
|
||||
weight: calculateWeight(dipTm, 0.3)));
|
||||
|
||||
accessories.add(createSimple('face_pull', 'Band Face Pull', 3, 10));
|
||||
accessories.add(createSimple('ab_roll', 'Ab Wheel Rollout', 3, 10));
|
||||
break;
|
||||
|
||||
case 3: // Pullup Tag (Pull)
|
||||
// Curls: ~20% vom System-Pullup-TM
|
||||
accessories.add(createSimple('curl', 'Barbell Curl', 3, 10,
|
||||
weight: calculateWeight(pullupTm, 0.2)));
|
||||
|
||||
accessories.add(_createIntervalExercise(
|
||||
id: 'kb_snatch_acc',
|
||||
name: 'KB Snatch',
|
||||
sets: 10,
|
||||
intervalSeconds: 60,
|
||||
repsPerSet: 5));
|
||||
accessories.add(createSimple('plank', 'Plank (30s)', 3, 1));
|
||||
break;
|
||||
}
|
||||
return accessories;
|
||||
}
|
||||
|
||||
List<Exercise> _generateConditioning(int day, int targetSets) {
|
||||
final accessories = <Exercise>[];
|
||||
|
||||
const totalTimeSeconds = 20 * 60;
|
||||
final intervalSeconds = (totalTimeSeconds / targetSets).floor();
|
||||
|
||||
String id;
|
||||
String name;
|
||||
|
||||
switch (day) {
|
||||
case 1:
|
||||
id = 'kb_clean_press';
|
||||
name = 'KB Clean & Press';
|
||||
break;
|
||||
case 2:
|
||||
id = 'kb_snatch_cond';
|
||||
name = 'KB Snatch';
|
||||
break;
|
||||
case 3:
|
||||
id = 'kb_thruster';
|
||||
name = 'KB Thruster';
|
||||
break;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
||||
accessories.add(_createIntervalExercise(
|
||||
id: id,
|
||||
name: name,
|
||||
sets: targetSets,
|
||||
intervalSeconds: intervalSeconds,
|
||||
repsPerSet: 5,
|
||||
));
|
||||
|
||||
return accessories;
|
||||
}
|
||||
|
||||
Exercise _createIntervalExercise({
|
||||
required String id,
|
||||
required String name,
|
||||
required int sets,
|
||||
required int intervalSeconds,
|
||||
required int repsPerSet,
|
||||
}) {
|
||||
return Exercise(
|
||||
exerciseId: id,
|
||||
exerciseName: '$name (${_formatIntervalName(intervalSeconds)})',
|
||||
bodyweightAtSession: 0,
|
||||
intervalSeconds: intervalSeconds,
|
||||
sets: List.generate(
|
||||
sets,
|
||||
(i) => WorkoutSet(
|
||||
setNumber: i + 1,
|
||||
repsTarget: repsPerSet,
|
||||
targetWeightTotal: 0,
|
||||
repsActual: 0,
|
||||
isAmrap: false,
|
||||
completed: false,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatIntervalName(int seconds) {
|
||||
if (seconds == 60) return 'EMOM';
|
||||
return 'E${seconds}S';
|
||||
}
|
||||
}
|
||||
|
|
@ -16,9 +16,9 @@ import '../../../../shared/data/repositories/cycle_repository.dart';
|
|||
import '../../../../shared/data/repositories/workout_repository.dart';
|
||||
import '../../../../shared/data/remote/sync_service.dart';
|
||||
import '../widgets/plate_visualizer.dart';
|
||||
import '../widgets/timer_widget.dart';
|
||||
import '../widgets/enemy_hp_bar.dart';
|
||||
import '../../../gamification/application/quest_service.dart';
|
||||
import '../widgets/emom_timer_widget.dart';
|
||||
|
||||
class BattleScreen extends ConsumerStatefulWidget {
|
||||
final int week;
|
||||
|
|
@ -73,6 +73,37 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
void _handleEmomSetComplete() {
|
||||
final currentExercise = _exercises[_currentExerciseIndex];
|
||||
final currentSet = currentExercise.sets[_currentSetIndex];
|
||||
|
||||
final updatedSet = currentSet.copyWith(
|
||||
repsActual: currentSet.repsTarget,
|
||||
completed: true,
|
||||
);
|
||||
|
||||
final updatedSets = List<WorkoutSet>.from(currentExercise.sets);
|
||||
updatedSets[_currentSetIndex] = updatedSet;
|
||||
|
||||
final updatedExercise = currentExercise.copyWith(sets: updatedSets);
|
||||
final updatedExercises = List<Exercise>.from(_exercises);
|
||||
updatedExercises[_currentExerciseIndex] = updatedExercise;
|
||||
|
||||
if (_currentSetIndex < currentExercise.sets.length - 1) {
|
||||
setState(() {
|
||||
_exercises = updatedExercises;
|
||||
_currentSetIndex++;
|
||||
|
||||
_repsCompleted = currentExercise.sets[_currentSetIndex].repsTarget;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_exercises = updatedExercises;
|
||||
});
|
||||
_showEmomFinishDialog();
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _getExerciseConfig(int day, UserCollection user) {
|
||||
final variants = user.exerciseVariants ?? {};
|
||||
|
||||
|
|
@ -146,56 +177,77 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
|
||||
Future<void> _loadWorkout() async {
|
||||
final userRepo = ref.read(userRepositoryProvider);
|
||||
final workoutRepo = ref.read(workoutRepositoryProvider);
|
||||
final cycleRepo = ref.read(cycleRepositoryProvider);
|
||||
|
||||
final user = await userRepo.getLocalUser();
|
||||
final trainingMaxesMap = await cycleRepo.getCurrentTrainingMaxesAsync();
|
||||
|
||||
if (user == null) {
|
||||
if (mounted) context.go('/hub');
|
||||
return;
|
||||
}
|
||||
|
||||
final exercises = <Exercise>[];
|
||||
final exerciseConfigs = _getExerciseConfig(widget.day, user);
|
||||
List<Exercise> exercises = [];
|
||||
|
||||
for (final config in exerciseConfigs) {
|
||||
final id = config['id'] as String;
|
||||
final name = config['name'] as String;
|
||||
final type = config['type'] as ExerciseType;
|
||||
final isMain = config['isMain'] as bool;
|
||||
if (widget.workoutId != null) {
|
||||
try {
|
||||
final allWorkouts = await workoutRepo.getAllWorkouts();
|
||||
|
||||
String tmKey = id;
|
||||
if (id == 'bench') tmKey = 'dip';
|
||||
if (id == 'row') tmKey = 'pullup';
|
||||
final loadedWorkout =
|
||||
allWorkouts.where((w) => w.id == widget.workoutId).firstOrNull;
|
||||
|
||||
final tm = trainingMaxesMap[tmKey] ?? 0.0;
|
||||
List<WorkoutSet> sets = [];
|
||||
if (loadedWorkout != null && loadedWorkout.exercises.isNotEmpty) {
|
||||
exercises = loadedWorkout.exercises.map((e) {
|
||||
return Exercise.fromJson(e as Map<String, dynamic>);
|
||||
}).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Fehler beim Laden des gespeicherten Workouts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (isMain) {
|
||||
sets = WendlerCalculator.generateSets(
|
||||
week: widget.week,
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: user.currentBodyweight,
|
||||
);
|
||||
} else {
|
||||
if (widget.week != 4) {
|
||||
sets = WendlerCalculator.generateFSLSets(
|
||||
if (exercises.isEmpty) {
|
||||
final trainingMaxesMap = await cycleRepo.getCurrentTrainingMaxesAsync();
|
||||
final exerciseConfigs = _getExerciseConfig(widget.day, user);
|
||||
|
||||
for (final config in exerciseConfigs) {
|
||||
final id = config['id'] as String;
|
||||
final name = config['name'] as String;
|
||||
final type = config['type'] as ExerciseType;
|
||||
final isMain = config['isMain'] as bool;
|
||||
|
||||
String tmKey = id;
|
||||
if (id == 'bench') tmKey = 'dip';
|
||||
if (id == 'row') tmKey = 'pullup';
|
||||
|
||||
final tm = trainingMaxesMap[tmKey] ?? 0.0;
|
||||
List<WorkoutSet> sets = [];
|
||||
|
||||
if (isMain) {
|
||||
sets = WendlerCalculator.generateSets(
|
||||
week: widget.week,
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: user.currentBodyweight,
|
||||
);
|
||||
} else {
|
||||
if (widget.week != 4) {
|
||||
sets = WendlerCalculator.generateFSLSets(
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: user.currentBodyweight,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sets.isNotEmpty) {
|
||||
exercises.add(Exercise(
|
||||
exerciseId: id,
|
||||
exerciseName: isMain ? name : '$name (FSL)',
|
||||
bodyweightAtSession: user.currentBodyweight,
|
||||
sets: sets,
|
||||
));
|
||||
if (sets.isNotEmpty) {
|
||||
exercises.add(Exercise(
|
||||
exerciseId: id,
|
||||
exerciseName: isMain ? name : '$name (FSL)',
|
||||
bodyweightAtSession: user.currentBodyweight,
|
||||
sets: sets,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -458,9 +510,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
return FutureBuilder<Map<String, dynamic>>(
|
||||
future: ref.read(userRepositoryProvider).getInventorySettingsAsync(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData)
|
||||
if (!snapshot.hasData) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
final inventory = snapshot.data!;
|
||||
|
||||
|
|
@ -482,7 +535,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
|
||||
final isTwoSided = currentExercise.exerciseId == 'squat' ||
|
||||
currentExercise.exerciseId == 'row' ||
|
||||
currentExercise.exerciseId == 'bench';
|
||||
currentExercise.exerciseId == 'bench' ||
|
||||
currentExercise.exerciseId == 'rdl' ||
|
||||
currentExercise.exerciseId == 'ohp' ||
|
||||
currentExercise.exerciseId == 'curl';
|
||||
final isBodyweight = !isTwoSided;
|
||||
final barWeight = isBodyweight
|
||||
? currentExercise.bodyweightAtSession
|
||||
|
|
@ -553,14 +609,16 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: _isResting
|
||||
? _buildRestScreen()
|
||||
: _buildWorkoutScreen(currentExercise, currentSet,
|
||||
plateResult, completedHP, totalHP),
|
||||
Positioned.fill(
|
||||
child: SafeArea(
|
||||
child: _isResting
|
||||
? _buildRestScreen()
|
||||
: _buildWorkoutScreen(currentExercise, currentSet,
|
||||
plateResult, completedHP, totalHP),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -633,6 +691,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
int completedHP,
|
||||
int totalHP,
|
||||
) {
|
||||
if (currentExercise.intervalSeconds != null &&
|
||||
currentExercise.intervalSeconds! > 0) {
|
||||
return _buildEmomView(currentExercise, currentSet, completedHP, totalHP);
|
||||
}
|
||||
final readableStyle = Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
|
|
@ -660,7 +722,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
child: Image.asset(
|
||||
_getEnemyAsset(currentExercise.exerciseId),
|
||||
fit: BoxFit.contain,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
colorBlendMode: BlendMode.modulate,
|
||||
),
|
||||
),
|
||||
|
|
@ -707,12 +769,12 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
flex: 6,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surfaceColor.withOpacity(0.95),
|
||||
color: AppTheme.surfaceColor.withValues(alpha: 0.95),
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, -5))
|
||||
],
|
||||
|
|
@ -760,7 +822,8 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
color:
|
||||
AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.primaryColor),
|
||||
),
|
||||
|
|
@ -925,6 +988,263 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmomView(
|
||||
Exercise currentExercise,
|
||||
WorkoutSet currentSet,
|
||||
int completedHP,
|
||||
int totalHP,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surfaceColor.withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 60,
|
||||
width: 60,
|
||||
child: Image.asset(
|
||||
_getEnemyAsset(currentExercise.exerciseId),
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (c, o, s) => const Icon(Icons.fitness_center,
|
||||
size: 40, color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
currentExercise.exerciseName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
'${currentSet.repsTarget} Reps per Round',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'${totalHP - completedHP}/$totalHP HP',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.errorColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: totalHP > 0
|
||||
? (totalHP - completedHP) / totalHP
|
||||
: 0.0,
|
||||
backgroundColor: Colors.red[900],
|
||||
color: AppTheme.errorColor,
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
EmomTimerWidget(
|
||||
key: ValueKey(
|
||||
'${currentExercise.exerciseId}_$_currentExerciseIndex'),
|
||||
intervalSeconds: currentExercise.intervalSeconds!,
|
||||
totalSets: currentExercise.sets.length,
|
||||
currentSet: _currentSetIndex + 1,
|
||||
onSetComplete: _handleEmomSetComplete,
|
||||
onWorkoutComplete: _handleEmomSetComplete,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
if (currentSet.targetWeightTotal > 0)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppTheme.primaryColor),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'WEIGHT: ${currentSet.targetWeightTotal} kg',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _adjustEmomSets(int newTotalSets) {
|
||||
final currentEx = _exercises[_currentExerciseIndex];
|
||||
|
||||
if (newTotalSets == currentEx.sets.length) return;
|
||||
|
||||
List<WorkoutSet> currentSets = List.from(currentEx.sets);
|
||||
|
||||
if (newTotalSets > currentSets.length) {
|
||||
final templateSet = currentSets.last;
|
||||
|
||||
for (int i = currentSets.length; i < newTotalSets; i++) {
|
||||
currentSets.add(templateSet.copyWith(
|
||||
setNumber: i + 1,
|
||||
completed: true,
|
||||
repsActual: templateSet.repsTarget,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
currentSets = currentSets.sublist(0, newTotalSets);
|
||||
}
|
||||
|
||||
final updatedEx = currentEx.copyWith(sets: currentSets);
|
||||
final updatedExercises = List<Exercise>.from(_exercises);
|
||||
updatedExercises[_currentExerciseIndex] = updatedEx;
|
||||
|
||||
setState(() {
|
||||
_exercises = updatedExercises;
|
||||
|
||||
_currentSetIndex = newTotalSets - 1;
|
||||
|
||||
_repsCompleted = updatedEx.sets.last.repsTarget;
|
||||
});
|
||||
}
|
||||
|
||||
void _showEmomFinishDialog() {
|
||||
final currentEx = _exercises[_currentExerciseIndex];
|
||||
int setsCount = currentEx.sets.length;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: AppTheme.surfaceColor,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.timer_off,
|
||||
size: 48, color: AppTheme.primaryColor),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'MISSION ACCOMPLISHED',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Time is up. Did you push further?',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_CounterButton(
|
||||
icon: Icons.remove,
|
||||
onTap: setsCount > 1
|
||||
? () => setModalState(() => setsCount--)
|
||||
: null,
|
||||
),
|
||||
Container(
|
||||
width: 140,
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'$setsCount',
|
||||
style: const TextStyle(
|
||||
fontSize: 64,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const Text('SETS COMPLETED',
|
||||
style: TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
_CounterButton(
|
||||
icon: Icons.add,
|
||||
onTap: () => setModalState(() => setsCount++),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
|
||||
_adjustEmomSets(setsCount);
|
||||
|
||||
_completeSet();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.successColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('CONFIRM & FINISH',
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoBox extends StatelessWidget {
|
||||
|
|
@ -968,7 +1288,7 @@ class _CounterButton extends StatelessWidget {
|
|||
decoration: BoxDecoration(
|
||||
color: onTap != null
|
||||
? AppTheme.primaryColor
|
||||
: Colors.grey.withOpacity(0.1),
|
||||
: Colors.grey.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,232 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import '../../../../core/constants/asset_paths.dart';
|
||||
|
||||
class EmomTimerWidget extends StatefulWidget {
|
||||
final int intervalSeconds;
|
||||
final int totalSets;
|
||||
final int currentSet;
|
||||
final VoidCallback onSetComplete;
|
||||
final VoidCallback onWorkoutComplete;
|
||||
|
||||
const EmomTimerWidget({
|
||||
super.key,
|
||||
required this.intervalSeconds,
|
||||
required this.totalSets,
|
||||
required this.currentSet,
|
||||
required this.onSetComplete,
|
||||
required this.onWorkoutComplete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EmomTimerWidget> createState() => _EmomTimerWidgetState();
|
||||
}
|
||||
|
||||
class _EmomTimerWidgetState extends State<EmomTimerWidget>
|
||||
with TickerProviderStateMixin {
|
||||
Timer? _timer;
|
||||
late int _secondsRemaining;
|
||||
bool _isRunning = false;
|
||||
late AnimationController _pulseController;
|
||||
late AudioPlayer _audioPlayer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_secondsRemaining = widget.intervalSeconds;
|
||||
_audioPlayer = AudioPlayer();
|
||||
|
||||
_pulseController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
lowerBound: 1.0,
|
||||
upperBound: 1.1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_pulseController.dispose();
|
||||
_audioPlayer.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _playSound(bool isLong) async {
|
||||
try {
|
||||
final path = isLong ? 'audio/beep_long.ogg' : 'audio/beep_short.ogg';
|
||||
|
||||
if (_audioPlayer.state == PlayerState.playing) {
|
||||
await _audioPlayer.stop();
|
||||
}
|
||||
await _audioPlayer.play(AssetSource(path));
|
||||
} catch (e) {
|
||||
debugPrint('Audio error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
setState(() => _isRunning = true);
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_secondsRemaining > 0) {
|
||||
setState(() => _secondsRemaining--);
|
||||
if (_secondsRemaining <= 3) {
|
||||
_pulseController.forward().then((_) => _pulseController.reverse());
|
||||
_playSound(false);
|
||||
}
|
||||
} else {
|
||||
_playSound(true);
|
||||
_handleRoundComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleRoundComplete() {
|
||||
if (widget.currentSet < widget.totalSets) {
|
||||
widget.onSetComplete();
|
||||
setState(() {
|
||||
_secondsRemaining = widget.intervalSeconds;
|
||||
});
|
||||
} else {
|
||||
_timer?.cancel();
|
||||
setState(() => _isRunning = false);
|
||||
widget.onWorkoutComplete();
|
||||
}
|
||||
}
|
||||
|
||||
void _pauseTimer() {
|
||||
_timer?.cancel();
|
||||
setState(() => _isRunning = false);
|
||||
}
|
||||
|
||||
String _formatTime(int seconds) {
|
||||
final m = seconds ~/ 60;
|
||||
final s = seconds % 60;
|
||||
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final progress = 1.0 - (_secondsRemaining / widget.intervalSeconds);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppTheme.primaryColor),
|
||||
),
|
||||
child: Text(
|
||||
'ROUND ${widget.currentSet} / ${widget.totalSets}',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ScaleTransition(
|
||||
scale: _pulseController,
|
||||
child: SizedBox(
|
||||
width: 240,
|
||||
height: 240,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 240,
|
||||
height: 240,
|
||||
child: CircularProgressIndicator(
|
||||
value: 1.0,
|
||||
strokeWidth: 12,
|
||||
color: Colors.white10,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 240,
|
||||
height: 240,
|
||||
child: CircularProgressIndicator(
|
||||
value: progress,
|
||||
strokeWidth: 12,
|
||||
color: _secondsRemaining <= 3
|
||||
? AppTheme.errorColor
|
||||
: AppTheme.primaryColor,
|
||||
strokeCap: StrokeCap.round,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_formatTime(_secondsRemaining),
|
||||
style: Theme.of(context).textTheme.displayLarge?.copyWith(
|
||||
fontSize: 64,
|
||||
color: Colors.white,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
if (!_isRunning &&
|
||||
widget.currentSet == 1 &&
|
||||
_secondsRemaining == widget.intervalSeconds)
|
||||
Text(
|
||||
'READY?',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(color: Colors.grey),
|
||||
)
|
||||
else if (!_isRunning)
|
||||
Text(
|
||||
'PAUSED',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(color: AppTheme.errorColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
if (!_isRunning)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _startTimer,
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
label: Text(widget.currentSet == 1 &&
|
||||
_secondsRemaining == widget.intervalSeconds
|
||||
? 'IGNITE ENGINE'
|
||||
: 'RESUME'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.successColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _pauseTimer,
|
||||
icon: const Icon(Icons.pause),
|
||||
label: const Text('PAUSE'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppTheme.errorColor,
|
||||
side: const BorderSide(color: AppTheme.errorColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ class EnemyHPBar extends StatelessWidget {
|
|||
color: Colors.red[900],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppTheme.errorColor.withOpacity(0.5),
|
||||
color: AppTheme.errorColor.withValues(alpha: 0.5),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
|
|
@ -72,7 +72,7 @@ class EnemyHPBar extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.errorColor.withOpacity(0.5),
|
||||
color: AppTheme.errorColor.withValues(alpha: 0.5),
|
||||
blurRadius: 8,
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class PlateVisualizer extends StatelessWidget {
|
|||
Icon(
|
||||
isTwoSided ? Icons.fitness_center : Icons.accessibility,
|
||||
size: 64,
|
||||
color: AppTheme.primaryColor.withOpacity(0.5),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -1,109 +1,3 @@
|
|||
// import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
// import 'package:drift/drift.dart';
|
||||
// import '../local/app_database.dart';
|
||||
// import '../remote/api_client.dart';
|
||||
// import '../../../../main.dart';
|
||||
// import 'user_repository.dart';
|
||||
|
||||
// final workoutRepositoryProvider = Provider<WorkoutRepository>((ref) {
|
||||
// final db = ref.watch(appDatabaseProvider);
|
||||
// final apiClient = ref.watch(apiClientProvider);
|
||||
// return WorkoutRepository(db: db, apiClient: apiClient);
|
||||
// });
|
||||
|
||||
// class WorkoutRepository {
|
||||
// final AppDatabase db;
|
||||
// final ApiClient apiClient;
|
||||
|
||||
// WorkoutRepository({required this.db, required this.apiClient});
|
||||
|
||||
// Future<List<WorkoutCollection>> getAllWorkouts() async {
|
||||
// return await db.select(db.workouts).get();
|
||||
// }
|
||||
|
||||
// Future<List<WorkoutCollection>> getWorkoutsForCycle(String cycleId) async {
|
||||
// return await (db.select(db.workouts)
|
||||
// ..where((w) => w.cycleId.equals(cycleId)))
|
||||
// .get();
|
||||
// }
|
||||
|
||||
// Future<List<WorkoutCollection>> getCompletedWorkouts(String userId) async {
|
||||
// return await (db.select(db.workouts)
|
||||
// ..where((w) => w.userId.equals(userId) & w.completedAt.isNotNull()))
|
||||
// .get();
|
||||
// }
|
||||
|
||||
// Future<void> saveWorkout(WorkoutCollection workout) async {
|
||||
// final companion = workout.toCompanion(true).copyWith(
|
||||
// updatedAt: Value(DateTime.now()),
|
||||
// isDirty: const Value(true),
|
||||
// );
|
||||
// await db.into(db.workouts).insertOnConflictUpdate(companion);
|
||||
// }
|
||||
|
||||
// Future<WorkoutCollection> createWorkout({
|
||||
// required String userId,
|
||||
// required String cycleId,
|
||||
// required int week,
|
||||
// required int day,
|
||||
// required List<dynamic> exercises,
|
||||
// }) async {
|
||||
// final companion = WorkoutsCompanion(
|
||||
// userId: Value(userId),
|
||||
// cycleId: Value(cycleId),
|
||||
// week: Value(week),
|
||||
// day: Value(day),
|
||||
// exercises: Value(exercises),
|
||||
// scheduledDate: Value(DateTime.now()),
|
||||
// xpEarned: const Value(0),
|
||||
// notes: const Value(''),
|
||||
// isDirty: const Value(true),
|
||||
// createdAt: Value(DateTime.now()),
|
||||
// updatedAt: Value(DateTime.now()),
|
||||
// );
|
||||
|
||||
// final id = await db.into(db.workouts).insert(companion);
|
||||
// return await (db.select(db.workouts)..where((w) => w.id.equals(id)))
|
||||
// .getSingle();
|
||||
// }
|
||||
|
||||
// Future<void> completeWorkout(
|
||||
// WorkoutCollection workout, {
|
||||
// required int xpEarned,
|
||||
// }) async {
|
||||
// final companion = WorkoutsCompanion(
|
||||
// id: Value(workout.id),
|
||||
// completedAt: Value(DateTime.now()),
|
||||
// xpEarned: Value(xpEarned),
|
||||
// exercises: Value(workout.exercises),
|
||||
// isDirty: const Value(true),
|
||||
// updatedAt: Value(DateTime.now()),
|
||||
// );
|
||||
|
||||
// await (db.update(db.workouts)..where((w) => w.id.equals(workout.id)))
|
||||
// .write(companion);
|
||||
// }
|
||||
|
||||
// Future<WorkoutCollection?> getWorkoutByWeekDay({
|
||||
// required String cycleId,
|
||||
// String? localCycleId,
|
||||
// required int week,
|
||||
// required int day,
|
||||
// }) async {
|
||||
// return await (db.select(db.workouts)
|
||||
// ..where((w) {
|
||||
// final weekDayCheck = w.week.equals(week) & w.day.equals(day);
|
||||
|
||||
// Expression<bool> cycleCheck = w.cycleId.equals(cycleId);
|
||||
// if (localCycleId != null) {
|
||||
// cycleCheck = cycleCheck | w.cycleId.equals(localCycleId);
|
||||
// }
|
||||
|
||||
// return weekDayCheck & cycleCheck;
|
||||
// }))
|
||||
// .getSingleOrNull();
|
||||
// }
|
||||
// }
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import '../local/app_database.dart';
|
||||
|
|
@ -210,4 +104,9 @@ class WorkoutRepository {
|
|||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<WorkoutCollection?> getWorkoutById(int id) async {
|
||||
return await (db.select(db.workouts)..where((w) => w.id.equals(id)))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ class Exercise with _$Exercise {
|
|||
required String exerciseName,
|
||||
@Default(0.0) double bodyweightAtSession,
|
||||
@Default([]) List<WorkoutSet> sets,
|
||||
int? intervalSeconds,
|
||||
}) = _Exercise;
|
||||
|
||||
factory Exercise.fromJson(Map<String, dynamic> json) =>
|
||||
_$ExerciseFromJson(json);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,30 @@ import 'dart:math';
|
|||
import '../entities/workout_set.dart';
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
|
||||
enum ExerciseType { squat, pullup, dip, row, bench }
|
||||
enum ExerciseType {
|
||||
// Main Lifts
|
||||
squat,
|
||||
pullup,
|
||||
dip,
|
||||
row,
|
||||
bench,
|
||||
|
||||
// Hypertrophy Accessories
|
||||
deadlift_romanian,
|
||||
curl_barbell,
|
||||
press_overhead,
|
||||
face_pull,
|
||||
ab_wheel,
|
||||
plank,
|
||||
|
||||
// Conditioning (Kettlebell)
|
||||
kb_swing,
|
||||
kb_snatch,
|
||||
kb_thruster,
|
||||
kb_clean_press
|
||||
}
|
||||
|
||||
enum AccessoryTemplate { none, hypertrophy, conditioning }
|
||||
|
||||
class WendlerCalculator {
|
||||
static const Map<int, List<double>> weekPercentages = {
|
||||
|
|
|
|||
56
pubspec.lock
56
pubspec.lock
|
|
@ -41,6 +41,62 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
audioplayers:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audioplayers
|
||||
sha256: "5441fa0ceb8807a5ad701199806510e56afde2b4913d9d17c2f19f2902cf0ae4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.1"
|
||||
audioplayers_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_android
|
||||
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.1"
|
||||
audioplayers_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_darwin
|
||||
sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
audioplayers_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_linux
|
||||
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
audioplayers_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_platform_interface
|
||||
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.1.1"
|
||||
audioplayers_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_web
|
||||
sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
audioplayers_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_windows
|
||||
sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
audioplayers: ^6.0.0
|
||||
|
||||
# State Management
|
||||
flutter_riverpod: ^2.5.1
|
||||
|
|
@ -67,6 +68,7 @@ flutter:
|
|||
- assets/images/plates/
|
||||
- assets/images/enemies/
|
||||
- assets/images/backgrounds/
|
||||
- assets/audio/
|
||||
|
||||
# fonts:
|
||||
# - family: PixelFont
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue