refactor: clean up smaller fixes

This commit is contained in:
Patryk Hegenberg 2026-01-04 10:56:06 +01:00
parent 2609446e9a
commit 32e935b3ec
33 changed files with 1292 additions and 1389 deletions

BIN
assets/audio/beep_long.ogg Normal file

Binary file not shown.

BIN
assets/audio/beep_short.ogg Normal file

Binary file not shown.

View file

@ -13,14 +13,14 @@ class AppConstants {
// XP System // XP System
static const int baseXP = 1000; static const int baseXP = 1000;
static const double xpMultiplier = 1.15; static const double xpMultiplier = 1.25;
static const int maxLevel = 100; static const int maxLevel = 100;
// XP Rewards // XP Rewards
static const int workoutCompleteXP = 100; 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 amrapBonusXPPerRep = 25;
static const int prBonusXP = 500; static const int prBonusXP = 200;
static const int cycleCompleteXP = 500; static const int cycleCompleteXP = 500;
// Rounding Steps // Rounding Steps

View file

@ -43,6 +43,9 @@ class AssetPaths {
static String getAvatarPath(String gender, int variant) { static String getAvatarPath(String gender, int variant) {
return 'assets/images/avatars/$gender/$variant.png'; 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 { class PlateColors {

View file

@ -165,7 +165,7 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
), ),
Positioned.fill( Positioned.fill(
child: Container( child: Container(
color: Colors.black.withOpacity(0.5), color: Colors.black.withValues(alpha: 0.5),
), ),
), ),
Center( Center(
@ -176,11 +176,11 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
width: 120, width: 120,
height: 120, height: 120,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF00E5FF).withOpacity(0.9), color: const Color(0xFF00E5FF).withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: const Color(0xFF00E5FF).withOpacity(0.6), color: const Color(0xFF00E5FF).withValues(alpha: 0.6),
blurRadius: 20, blurRadius: 20,
spreadRadius: 5, spreadRadius: 5,
), ),

View file

@ -87,7 +87,7 @@ class AppTheme {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
side: BorderSide( side: BorderSide(
color: primaryColor.withOpacity(0.3), color: primaryColor.withValues(alpha: 0.3),
width: 1, width: 1,
), ),
), ),
@ -97,11 +97,11 @@ class AppTheme {
fillColor: surfaceColor, fillColor: surfaceColor,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: primaryColor.withOpacity(0.5)), borderSide: BorderSide(color: primaryColor.withValues(alpha: 0.5)),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: primaryColor.withOpacity(0.3)), borderSide: BorderSide(color: primaryColor.withValues(alpha: 0.3)),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),

View file

@ -10,6 +10,7 @@ import '../../../gamification/domain/entities/avatar_config.dart';
import '../../../gamification/presentation/widgets/avatar_editor.dart'; import '../../../gamification/presentation/widgets/avatar_editor.dart';
import '../../../gamification/presentation/widgets/avatar_renderer.dart'; import '../../../gamification/presentation/widgets/avatar_renderer.dart';
import '../../../gamification/domain/entities/item_catalog.dart'; import '../../../gamification/domain/entities/item_catalog.dart';
import '../../../../shared/domain/logic/wendler_calculator.dart';
class ProfileScreen extends ConsumerStatefulWidget { class ProfileScreen extends ConsumerStatefulWidget {
const ProfileScreen({super.key}); const ProfileScreen({super.key});
@ -176,11 +177,10 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
Navigator.pop(context); Navigator.pop(context);
setState(() => _isLoading = true); setState(() => _isLoading = true);
// Update Config
final newConfig = AvatarConfig( final newConfig = AvatarConfig(
gender: currentConfig.gender, gender: currentConfig.gender,
variant: currentConfig.variant, variant: currentConfig.variant,
selectedBackground: item.id, // Hintergrund setzen selectedBackground: item.id,
); );
final updatedUser = _user!.copyWith( final updatedUser = _user!.copyWith(
@ -193,12 +193,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
.read(userRepositoryProvider) .read(userRepositoryProvider)
.saveLocalUser(_user!); .saveLocalUser(_user!);
setState(() => _isLoading = false); setState(() => _isLoading = false);
// Save to DB
// await ref
// .read(userRepositoryProvider)
// .updateAvatarConfig(newConfig.toJson());
// await _loadUser();
// setState(() => _isLoading = false);
} }
: null, : null,
child: Container( child: Container(
@ -256,7 +250,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
child: Container( child: Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7), color: Colors.black.withValues(alpha: 0.7),
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(10)), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userRepo = ref.watch(userRepositoryProvider); final userRepo = ref.watch(userRepositoryProvider);
@ -411,7 +447,6 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// const SizedBox(height: 16),
Center( Center(
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: _showBackgroundSelector, onPressed: _showBackgroundSelector,
@ -441,7 +476,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
value: _currentBodyweight, value: _currentBodyweight,
min: 40, min: 40,
max: 150, max: 150,
divisions: 220, // 0.5 steps divisions: 220,
label: _currentBodyweight.toStringAsFixed(1), label: _currentBodyweight.toStringAsFixed(1),
activeColor: AppTheme.primaryColor, activeColor: AppTheme.primaryColor,
onChanged: (val) => setState(() { onChanged: (val) => setState(() {
@ -467,6 +502,26 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
), ),
), ),
const SizedBox(height: 32), 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', Text('Account Security',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
@ -489,10 +544,10 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
const SizedBox(height: 8), const SizedBox(height: 8),
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: border: Border.all(
Border.all(color: AppTheme.errorColor.withOpacity(0.5)), color: AppTheme.errorColor.withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
color: AppTheme.errorColor.withOpacity(0.05), color: AppTheme.errorColor.withValues(alpha: 0.05),
), ),
child: Column( child: Column(
children: [ 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,
);
}
} }

View file

@ -1,10 +1,7 @@
// import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
// import '../../../../core/constants/asset_paths.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../shared/data/local/app_database.dart'; import '../../../../shared/data/local/app_database.dart';
import '../../../../shared/data/repositories/user_repository.dart'; import '../../../../shared/data/repositories/user_repository.dart';
@ -23,6 +20,7 @@ import '../widgets/xp_bar_widget.dart';
import '../widgets/level_display.dart'; import '../widgets/level_display.dart';
import '../widgets/start_raid_button.dart'; import '../widgets/start_raid_button.dart';
import '../../../gamification/application/quest_service.dart'; import '../../../gamification/application/quest_service.dart';
import '../../../workout_runner/application/workout_generator_service.dart';
class HubScreen extends ConsumerStatefulWidget { class HubScreen extends ConsumerStatefulWidget {
const HubScreen({super.key}); 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( Future<void> _startNextWorkout(
CycleCollection cycle, UserCollection user) async { CycleCollection cycle, UserCollection user) async {
try { try {
final workoutRepo = ref.read(workoutRepositoryProvider); final workoutRepo = ref.read(workoutRepositoryProvider);
final workoutGenerator = ref.read(workoutGeneratorServiceProvider);
final tmsDynamic = cycle.trainingMaxes; final tmsDynamic = cycle.trainingMaxes;
final trainingMaxes = Map<String, double>.from(tmsDynamic final trainingMaxes = Map<String, double>.from(tmsDynamic
@ -181,7 +88,6 @@ class _HubScreenState extends ConsumerState<HubScreen> {
} }
return; return;
} }
var workout = await workoutRepo.getWorkoutByWeekDay( var workout = await workoutRepo.getWorkoutByWeekDay(
cycleId: cycleRefId, cycleId: cycleRefId,
localCycleId: localCycleId, localCycleId: localCycleId,
@ -190,12 +96,22 @@ class _HubScreenState extends ConsumerState<HubScreen> {
); );
if (workout == null) { if (workout == null) {
final exercises = _generateExercises( final activeTemplate = _getTemplateFromUser(user);
week: targetWeek, int? conditioningSets;
day: targetDay,
trainingMaxes: trainingMaxes, if (activeTemplate == AccessoryTemplate.conditioning) {
bodyweight: user.currentBodyweight, conditioningSets = await _showConditioningDialog();
user: user); 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(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userRepo = ref.watch(userRepositoryProvider); final userRepo = ref.watch(userRepositoryProvider);
@ -280,9 +278,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: [
// Colors.black.withOpacity(0.6),
Colors.black.withValues(alpha: 0.6), Colors.black.withValues(alpha: 0.6),
// Colors.black.withOpacity(0.85),
Colors.black.withValues(alpha: 0.85), Colors.black.withValues(alpha: 0.85),
], ],
), ),
@ -389,7 +385,6 @@ class _HubScreenState extends ConsumerState<HubScreen> {
top: Radius.circular(24)), top: Radius.circular(24)),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
// color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2), color: Colors.black.withValues(alpha: 0.2),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, -5), offset: const Offset(0, -5),
@ -435,10 +430,6 @@ class _HubScreenState extends ConsumerState<HubScreen> {
} }
} }
// extension on Object {
// operator [](String other) {}
// }
class _StatBox extends StatelessWidget { class _StatBox extends StatelessWidget {
final String label; final String label;
final String value; final String value;
@ -456,7 +447,6 @@ class _StatBox extends StatelessWidget {
color: AppTheme.surfaceColor, color: AppTheme.surfaceColor,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
// color: AppTheme.primaryColor.withOpacity(0.3),
color: AppTheme.primaryColor.withValues(alpha: 0.3), color: AppTheme.primaryColor.withValues(alpha: 0.3),
), ),
), ),

View file

@ -23,7 +23,7 @@ class LevelDisplay extends StatelessWidget {
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.5), color: AppTheme.primaryColor.withValues(alpha: 0.5),
blurRadius: 12, blurRadius: 12,
spreadRadius: 2, spreadRadius: 2,
), ),
@ -57,4 +57,3 @@ class LevelDisplay extends StatelessWidget {
); );
} }
} }

View file

@ -54,7 +54,8 @@ class _StartRaidButtonState extends State<StartRaidButton>
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: AppTheme.primaryColor.withOpacity(_glowAnimation.value), color: AppTheme.primaryColor
.withValues(alpha: _glowAnimation.value),
blurRadius: 20, blurRadius: 20,
spreadRadius: 5, spreadRadius: 5,
), ),
@ -96,4 +97,3 @@ class _StartRaidButtonState extends State<StartRaidButton>
); );
} }
} }

View file

@ -49,7 +49,7 @@ class XPBarWidget extends StatelessWidget {
color: AppTheme.xpBarBackground, color: AppTheme.xpBarBackground,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.3), color: AppTheme.primaryColor.withValues(alpha: 0.3),
width: 2, width: 2,
), ),
), ),
@ -64,13 +64,13 @@ class XPBarWidget extends StatelessWidget {
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
AppTheme.primaryColor, AppTheme.primaryColor,
AppTheme.primaryColor.withOpacity(0.7), AppTheme.primaryColor.withValues(alpha: 0.7),
], ],
), ),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.5), color: AppTheme.primaryColor.withValues(alpha: 0.5),
blurRadius: 8, blurRadius: 8,
spreadRadius: 1, spreadRadius: 1,
), ),
@ -86,15 +86,15 @@ class XPBarWidget extends StatelessWidget {
child: Text( child: Text(
'${(progress * 100).toStringAsFixed(0)}%', '${(progress * 100).toStringAsFixed(0)}%',
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
shadows: [ shadows: [
const Shadow( const Shadow(
color: Colors.black, color: Colors.black,
blurRadius: 4, blurRadius: 4,
),
],
), ),
],
),
), ),
), ),
], ],
@ -103,4 +103,3 @@ class XPBarWidget extends StatelessWidget {
); );
} }
} }

View file

@ -83,14 +83,14 @@ class _LoreCard extends StatelessWidget {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Container( child: Container(
decoration: BoxDecoration( 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), borderRadius: BorderRadius.circular(16),
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: [
AppTheme.surfaceColor, AppTheme.surfaceColor,
color.withOpacity(0.1), color.withValues(alpha: 0.1),
], ],
), ),
), ),
@ -112,7 +112,7 @@ class _LoreCard extends StatelessWidget {
child: Image.asset( child: Image.asset(
assetPath, assetPath,
fit: BoxFit.contain, fit: BoxFit.contain,
color: Colors.white.withOpacity(0.9), color: Colors.white.withValues(alpha: 0.9),
colorBlendMode: BlendMode.modulate, colorBlendMode: BlendMode.modulate,
), ),
), ),

View file

@ -20,7 +20,7 @@ class AvatarRenderer extends StatelessWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.3), color: Colors.black.withValues(alpha: 0.3),
blurRadius: 10, blurRadius: 10,
spreadRadius: 2, spreadRadius: 2,
), ),

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/theme/app_theme.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 '../../data/repositories/quest_repository.dart';
import '../../domain/entities/item_catalog.dart'; import '../../domain/entities/item_catalog.dart';
@ -23,19 +23,9 @@ class _QuestItemState extends ConsumerState<QuestItem> {
Future<void> _handleClaim() async { Future<void> _handleClaim() async {
setState(() => _isClaiming = true); setState(() => _isClaiming = true);
try { 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); final questRepo = ref.read(questRepositoryProvider);
await questRepo.claimQuest(widget.quest.id); 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) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@ -52,7 +42,8 @@ class _QuestItemState extends ConsumerState<QuestItem> {
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e'))); ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Error: $e')));
} }
} finally { } finally {
if (mounted) setState(() => _isClaiming = false); if (mounted) setState(() => _isClaiming = false);
@ -61,7 +52,8 @@ class _QuestItemState extends ConsumerState<QuestItem> {
@override @override
Widget build(BuildContext context) { 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 isComplete = widget.quest.isCompleted;
final isClaimed = widget.quest.isClaimed; final isClaimed = widget.quest.isClaimed;
@ -69,7 +61,7 @@ class _QuestItemState extends ConsumerState<QuestItem> {
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
side: isComplete && !isClaimed side: isComplete && !isClaimed
? const BorderSide(color: AppTheme.successColor, width: 1) ? const BorderSide(color: AppTheme.successColor, width: 1)
: BorderSide.none, : BorderSide.none,
), ),
@ -83,7 +75,9 @@ class _QuestItemState extends ConsumerState<QuestItem> {
children: [ children: [
Icon( Icon(
_getIconForType(widget.quest.type), _getIconForType(widget.quest.type),
color: isComplete ? AppTheme.successColor : AppTheme.primaryColor, color: isComplete
? AppTheme.successColor
: AppTheme.primaryColor,
size: 20, size: 20,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@ -91,37 +85,40 @@ class _QuestItemState extends ConsumerState<QuestItem> {
child: Text( child: Text(
widget.quest.title, widget.quest.title,
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: isClaimed ? Colors.grey : Colors.white, color: isClaimed ? Colors.grey : Colors.white,
decoration: isClaimed ? TextDecoration.lineThrough : null, decoration:
), isClaimed ? TextDecoration.lineThrough : null,
),
), ),
), ),
if (isClaimed) if (isClaimed)
const Icon(Icons.check, color: Colors.grey, size: 20) const Icon(Icons.check, color: Colors.grey, size: 20)
else if (widget.quest.rewardXP > 0) else if (widget.quest.rewardXP > 0)
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.xpBarFill.withOpacity(0.2), color: AppTheme.xpBarFill.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
'+${widget.quest.rewardXP} XP', '+${widget.quest.rewardXP} XP',
style: const TextStyle( style: const TextStyle(
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold fontWeight: FontWeight.bold),
),
), ),
), ),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
widget.quest.description, 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), const SizedBox(height: 16),
@ -137,34 +134,42 @@ class _QuestItemState extends ConsumerState<QuestItem> {
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: progress, value: progress,
backgroundColor: Colors.grey[800], backgroundColor: Colors.grey[800],
color: isComplete ? AppTheme.successColor : AppTheme.primaryColor, color: isComplete
? AppTheme.successColor
: AppTheme.primaryColor,
minHeight: 8, minHeight: 8,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'${widget.quest.currentValue} / ${widget.quest.targetValue}', '${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), const SizedBox(width: 16),
if (isComplete && !isClaimed) if (isComplete && !isClaimed)
ElevatedButton( ElevatedButton(
onPressed: _isClaiming ? null : _handleClaim, onPressed: _isClaiming ? null : _handleClaim,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.successColor, backgroundColor: AppTheme.successColor,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 0),
minimumSize: const Size(0, 32), minimumSize: const Size(0, 32),
), ),
child: _isClaiming child: _isClaiming
? const SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) ? const SizedBox(
: const Text('CLAIM', style: TextStyle(fontSize: 12)), 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) 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) { IconData _getIconForType(String type) {
switch (type) { switch (type) {
case 'daily': return Icons.today; case 'daily':
case 'story': return Icons.auto_stories; return Icons.today;
case 'milestone': return Icons.emoji_events; case 'story':
default: return Icons.task_alt; return Icons.auto_stories;
case 'milestone':
return Icons.emoji_events;
default:
return Icons.task_alt;
} }
} }
} }

View file

@ -53,7 +53,7 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
Icon( Icon(
Icons.history_edu, Icons.history_edu,
size: 80, size: 80,
color: AppTheme.primaryColor.withOpacity(0.5), color: AppTheme.primaryColor.withValues(alpha: 0.5),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
@ -180,9 +180,9 @@ class _WorkoutHistoryCard extends StatelessWidget {
width: 50, width: 50,
height: 50, height: 50,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1), color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12), 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( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View file

@ -127,7 +127,9 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
final platesList = <double>[]; final platesList = <double>[];
_plateInventory.forEach((weight, count) { _plateInventory.forEach((weight, count) {
for (int i = 0; i < count; i++) platesList.add(weight); for (int i = 0; i < count; i++) {
platesList.add(weight);
}
}); });
final bandsList = <Map<String, dynamic>>[]; final bandsList = <Map<String, dynamic>>[];
@ -258,7 +260,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
?.copyWith(color: AppTheme.textSecondary)), ?.copyWith(color: AppTheme.textSecondary)),
const SizedBox(height: 8), const SizedBox(height: 8),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.vertical,
child: Row( child: Row(
children: [ children: [
ActionChip( ActionChip(
@ -315,7 +317,8 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
_hasChanges = true; _hasChanges = true;
}); });
}, },
selectedColor: _getBandColor(entry.key).withOpacity(0.3), selectedColor:
_getBandColor(entry.key).withValues(alpha: 0.3),
checkmarkColor: _getBandColor(entry.key), checkmarkColor: _getBandColor(entry.key),
side: BorderSide(color: _getBandColor(entry.key)), side: BorderSide(color: _getBandColor(entry.key)),
); );

View file

@ -68,10 +68,10 @@ class PlateCounter extends StatelessWidget {
height: 40, height: 40,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1), color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: border: Border.all(
Border.all(color: AppTheme.primaryColor.withOpacity(0.3)), color: AppTheme.primaryColor.withValues(alpha: 0.3)),
), ),
child: Text( child: Text(
count.toString(), count.toString(),

View file

@ -353,7 +353,8 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
_bandInventory[entry.key] = selected; _bandInventory[entry.key] = selected;
}); });
}, },
selectedColor: _getBandColor(entry.key).withOpacity(0.3), selectedColor:
_getBandColor(entry.key).withValues(alpha: 0.3),
checkmarkColor: _getBandColor(entry.key), checkmarkColor: _getBandColor(entry.key),
labelStyle: TextStyle( labelStyle: TextStyle(
color: entry.value ? Colors.white : Colors.grey, color: entry.value ? Colors.white : Colors.grey,

View file

@ -212,10 +212,10 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1), color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.3)), color: AppTheme.primaryColor.withValues(alpha: 0.3)),
), ),
child: Row( child: Row(
children: [ children: [
@ -281,7 +281,7 @@ class _ExerciseCard extends StatelessWidget {
children: [ children: [
Text(title.toUpperCase(), Text(title.toUpperCase(),
style: const TextStyle( style: const TextStyle(
color: Colors.grey, color: AppTheme.textSecondary,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -290,7 +290,7 @@ class _ExerciseCard extends StatelessWidget {
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.2), color: AppTheme.primaryColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8)), borderRadius: BorderRadius.circular(8)),
child: Icon(icon, color: AppTheme.primaryColor), child: Icon(icon, color: AppTheme.primaryColor),
), ),
@ -390,7 +390,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
children: [ children: [
Text(slotTitle.toUpperCase(), Text(slotTitle.toUpperCase(),
style: const TextStyle( style: const TextStyle(
color: Colors.grey, color: AppTheme.textSecondary,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),
Row( Row(
@ -403,7 +403,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
: Colors.grey)), : Colors.grey)),
Switch( Switch(
value: isCapable, value: isCapable,
activeColor: AppTheme.successColor, activeThumbColor: AppTheme.successColor,
onChanged: onToggleCapability, onChanged: onToggleCapability,
), ),
], ],
@ -416,7 +416,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.2), color: AppTheme.primaryColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8)), borderRadius: BorderRadius.circular(8)),
child: Icon(icon, color: AppTheme.primaryColor), child: Icon(icon, color: AppTheme.primaryColor),
), ),

View file

@ -20,7 +20,7 @@ class WelcomeScreen extends StatelessWidget {
), ),
Positioned.fill( Positioned.fill(
child: Container( child: Container(
color: Colors.black.withOpacity(0.7), color: Colors.black.withValues(alpha: 0.7),
), ),
), ),
SafeArea( SafeArea(
@ -35,11 +35,11 @@ class WelcomeScreen extends StatelessWidget {
width: 100, width: 100,
height: 100, height: 100,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.9), color: AppTheme.primaryColor.withValues(alpha: 0.9),
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.5), color: AppTheme.primaryColor.withValues(alpha: 0.5),
blurRadius: 20) blurRadius: 20)
], ],
), ),
@ -139,7 +139,7 @@ class _FeatureItem extends StatelessWidget {
width: 48, width: 48,
height: 48, height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.2), color: AppTheme.primaryColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Icon( child: Icon(

View file

@ -335,7 +335,7 @@ class _CurrentCycleCard extends StatelessWidget {
padding: padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4), const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.2), color: AppTheme.successColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: const Text( child: const Text(
@ -502,14 +502,15 @@ class _FilterChip extends StatelessWidget {
label: Text(label), label: Text(label),
selected: isSelected, selected: isSelected,
onSelected: (_) => onTap(), onSelected: (_) => onTap(),
selectedColor: AppTheme.primaryColor.withOpacity(0.2), selectedColor: AppTheme.primaryColor.withValues(alpha: 0.2),
labelStyle: TextStyle( labelStyle: TextStyle(
color: isSelected ? AppTheme.primaryColor : Colors.grey, color: isSelected ? AppTheme.primaryColor : Colors.grey,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
side: BorderSide( side: BorderSide(
color: color: isSelected
isSelected ? AppTheme.primaryColor : Colors.grey.withOpacity(0.3), ? AppTheme.primaryColor
: Colors.grey.withValues(alpha: 0.3),
), ),
); );
} }

View file

@ -51,7 +51,7 @@ class ProgressChart extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.surfaceColor, color: AppTheme.surfaceColor,
borderRadius: BorderRadius.circular(16), 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@ -148,14 +148,12 @@ class ProgressChart extends StatelessWidget {
), ),
belowBarData: BarAreaData( belowBarData: BarAreaData(
show: true, show: true,
color: AppTheme.primaryColor.withOpacity(0.1), color: AppTheme.primaryColor.withValues(alpha: 0.1),
), ),
), ),
], ],
lineTouchData: LineTouchData( lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData( touchTooltipData: LineTouchTooltipData(
// FIX 2: Alte API nutzen (tooltipBgColor statt getTooltipColor)
// tooltipBgColor: AppTheme.surfaceColor,
getTooltipColor: (touchedSpot) => AppTheme.surfaceColor, getTooltipColor: (touchedSpot) => AppTheme.surfaceColor,
getTooltipItems: (touchedSpots) { getTooltipItems: (touchedSpots) {
return touchedSpots.map((spot) { return touchedSpots.map((spot) {

View file

@ -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';
}
}

View file

@ -16,9 +16,9 @@ import '../../../../shared/data/repositories/cycle_repository.dart';
import '../../../../shared/data/repositories/workout_repository.dart'; import '../../../../shared/data/repositories/workout_repository.dart';
import '../../../../shared/data/remote/sync_service.dart'; import '../../../../shared/data/remote/sync_service.dart';
import '../widgets/plate_visualizer.dart'; import '../widgets/plate_visualizer.dart';
import '../widgets/timer_widget.dart';
import '../widgets/enemy_hp_bar.dart'; import '../widgets/enemy_hp_bar.dart';
import '../../../gamification/application/quest_service.dart'; import '../../../gamification/application/quest_service.dart';
import '../widgets/emom_timer_widget.dart';
class BattleScreen extends ConsumerStatefulWidget { class BattleScreen extends ConsumerStatefulWidget {
final int week; 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) { List<Map<String, dynamic>> _getExerciseConfig(int day, UserCollection user) {
final variants = user.exerciseVariants ?? {}; final variants = user.exerciseVariants ?? {};
@ -146,56 +177,77 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
Future<void> _loadWorkout() async { Future<void> _loadWorkout() async {
final userRepo = ref.read(userRepositoryProvider); final userRepo = ref.read(userRepositoryProvider);
final workoutRepo = ref.read(workoutRepositoryProvider);
final cycleRepo = ref.read(cycleRepositoryProvider); final cycleRepo = ref.read(cycleRepositoryProvider);
final user = await userRepo.getLocalUser(); final user = await userRepo.getLocalUser();
final trainingMaxesMap = await cycleRepo.getCurrentTrainingMaxesAsync();
if (user == null) { if (user == null) {
if (mounted) context.go('/hub'); if (mounted) context.go('/hub');
return; return;
} }
final exercises = <Exercise>[]; List<Exercise> exercises = [];
final exerciseConfigs = _getExerciseConfig(widget.day, user);
for (final config in exerciseConfigs) { if (widget.workoutId != null) {
final id = config['id'] as String; try {
final name = config['name'] as String; final allWorkouts = await workoutRepo.getAllWorkouts();
final type = config['type'] as ExerciseType;
final isMain = config['isMain'] as bool;
String tmKey = id; final loadedWorkout =
if (id == 'bench') tmKey = 'dip'; allWorkouts.where((w) => w.id == widget.workoutId).firstOrNull;
if (id == 'row') tmKey = 'pullup';
final tm = trainingMaxesMap[tmKey] ?? 0.0; if (loadedWorkout != null && loadedWorkout.exercises.isNotEmpty) {
List<WorkoutSet> sets = []; 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) { if (exercises.isEmpty) {
sets = WendlerCalculator.generateSets( final trainingMaxesMap = await cycleRepo.getCurrentTrainingMaxesAsync();
week: widget.week, final exerciseConfigs = _getExerciseConfig(widget.day, user);
trainingMax: tm,
exerciseType: type, for (final config in exerciseConfigs) {
currentBodyweight: user.currentBodyweight, final id = config['id'] as String;
); final name = config['name'] as String;
} else { final type = config['type'] as ExerciseType;
if (widget.week != 4) { final isMain = config['isMain'] as bool;
sets = WendlerCalculator.generateFSLSets(
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, trainingMax: tm,
exerciseType: type, exerciseType: type,
currentBodyweight: user.currentBodyweight, currentBodyweight: user.currentBodyweight,
); );
} else {
if (widget.week != 4) {
sets = WendlerCalculator.generateFSLSets(
trainingMax: tm,
exerciseType: type,
currentBodyweight: user.currentBodyweight,
);
}
} }
}
if (sets.isNotEmpty) { if (sets.isNotEmpty) {
exercises.add(Exercise( exercises.add(Exercise(
exerciseId: id, exerciseId: id,
exerciseName: isMain ? name : '$name (FSL)', exerciseName: isMain ? name : '$name (FSL)',
bodyweightAtSession: user.currentBodyweight, bodyweightAtSession: user.currentBodyweight,
sets: sets, sets: sets,
)); ));
}
} }
} }
@ -458,9 +510,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
return FutureBuilder<Map<String, dynamic>>( return FutureBuilder<Map<String, dynamic>>(
future: ref.read(userRepositoryProvider).getInventorySettingsAsync(), future: ref.read(userRepositoryProvider).getInventorySettingsAsync(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) if (!snapshot.hasData) {
return const Scaffold( return const Scaffold(
body: Center(child: CircularProgressIndicator())); body: Center(child: CircularProgressIndicator()));
}
final inventory = snapshot.data!; final inventory = snapshot.data!;
@ -482,7 +535,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
final isTwoSided = currentExercise.exerciseId == 'squat' || final isTwoSided = currentExercise.exerciseId == 'squat' ||
currentExercise.exerciseId == 'row' || currentExercise.exerciseId == 'row' ||
currentExercise.exerciseId == 'bench'; currentExercise.exerciseId == 'bench' ||
currentExercise.exerciseId == 'rdl' ||
currentExercise.exerciseId == 'ohp' ||
currentExercise.exerciseId == 'curl';
final isBodyweight = !isTwoSided; final isBodyweight = !isTwoSided;
final barWeight = isBodyweight final barWeight = isBodyweight
? currentExercise.bodyweightAtSession ? currentExercise.bodyweightAtSession
@ -553,14 +609,16 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
), ),
Positioned.fill( Positioned.fill(
child: Container( child: Container(
color: Colors.black.withOpacity(0.7), color: Colors.black.withValues(alpha: 0.7),
), ),
), ),
SafeArea( Positioned.fill(
child: _isResting child: SafeArea(
? _buildRestScreen() child: _isResting
: _buildWorkoutScreen(currentExercise, currentSet, ? _buildRestScreen()
plateResult, completedHP, totalHP), : _buildWorkoutScreen(currentExercise, currentSet,
plateResult, completedHP, totalHP),
),
), ),
], ],
), ),
@ -633,6 +691,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
int completedHP, int completedHP,
int totalHP, int totalHP,
) { ) {
if (currentExercise.intervalSeconds != null &&
currentExercise.intervalSeconds! > 0) {
return _buildEmomView(currentExercise, currentSet, completedHP, totalHP);
}
final readableStyle = Theme.of(context).textTheme.bodyLarge?.copyWith( final readableStyle = Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.white, color: Colors.white,
shadows: [ shadows: [
@ -660,7 +722,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
child: Image.asset( child: Image.asset(
_getEnemyAsset(currentExercise.exerciseId), _getEnemyAsset(currentExercise.exerciseId),
fit: BoxFit.contain, fit: BoxFit.contain,
color: Colors.white.withOpacity(0.9), color: Colors.white.withValues(alpha: 0.9),
colorBlendMode: BlendMode.modulate, colorBlendMode: BlendMode.modulate,
), ),
), ),
@ -707,12 +769,12 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
flex: 6, flex: 6,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.surfaceColor.withOpacity(0.95), color: AppTheme.surfaceColor.withValues(alpha: 0.95),
borderRadius: borderRadius:
const BorderRadius.vertical(top: Radius.circular(24)), const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.5), color: Colors.black.withValues(alpha: 0.5),
blurRadius: 20, blurRadius: 20,
offset: const Offset(0, -5)) offset: const Offset(0, -5))
], ],
@ -760,7 +822,8 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1), color:
AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppTheme.primaryColor), 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 { class _InfoBox extends StatelessWidget {
@ -968,7 +1288,7 @@ class _CounterButton extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: onTap != null color: onTap != null
? AppTheme.primaryColor ? AppTheme.primaryColor
: Colors.grey.withOpacity(0.1), : Colors.grey.withValues(alpha: 0.1),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon(icon, child: Icon(icon,

View file

@ -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),
),
),
),
],
);
}
}

View file

@ -53,7 +53,7 @@ class EnemyHPBar extends StatelessWidget {
color: Colors.red[900], color: Colors.red[900],
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: AppTheme.errorColor.withOpacity(0.5), color: AppTheme.errorColor.withValues(alpha: 0.5),
width: 2, width: 2,
), ),
), ),
@ -72,7 +72,7 @@ class EnemyHPBar extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: AppTheme.errorColor.withOpacity(0.5), color: AppTheme.errorColor.withValues(alpha: 0.5),
blurRadius: 8, blurRadius: 8,
), ),
], ],

View file

@ -30,7 +30,7 @@ class PlateVisualizer extends StatelessWidget {
Icon( Icon(
isTwoSided ? Icons.fitness_center : Icons.accessibility, isTwoSided ? Icons.fitness_center : Icons.accessibility,
size: 64, size: 64,
color: AppTheme.primaryColor.withOpacity(0.5), color: AppTheme.primaryColor.withValues(alpha: 0.5),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(

View file

@ -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:flutter_riverpod/flutter_riverpod.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import '../local/app_database.dart'; import '../local/app_database.dart';
@ -210,4 +104,9 @@ class WorkoutRepository {
..limit(1)) ..limit(1))
.getSingleOrNull(); .getSingleOrNull();
} }
Future<WorkoutCollection?> getWorkoutById(int id) async {
return await (db.select(db.workouts)..where((w) => w.id.equals(id)))
.getSingleOrNull();
}
} }

View file

@ -11,9 +11,9 @@ class Exercise with _$Exercise {
required String exerciseName, required String exerciseName,
@Default(0.0) double bodyweightAtSession, @Default(0.0) double bodyweightAtSession,
@Default([]) List<WorkoutSet> sets, @Default([]) List<WorkoutSet> sets,
int? intervalSeconds,
}) = _Exercise; }) = _Exercise;
factory Exercise.fromJson(Map<String, dynamic> json) => factory Exercise.fromJson(Map<String, dynamic> json) =>
_$ExerciseFromJson(json); _$ExerciseFromJson(json);
} }

View file

@ -2,7 +2,30 @@ import 'dart:math';
import '../entities/workout_set.dart'; import '../entities/workout_set.dart';
import '../../../core/constants/app_constants.dart'; import '../../../core/constants/app_constants.dart';
enum ExerciseType { squat, pullup, dip, 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 { class WendlerCalculator {
static const Map<int, List<double>> weekPercentages = { static const Map<int, List<double>> weekPercentages = {

View file

@ -41,6 +41,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" 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: boolean_selector:
dependency: transitive dependency: transitive
description: description:

View file

@ -9,6 +9,7 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
audioplayers: ^6.0.0
# State Management # State Management
flutter_riverpod: ^2.5.1 flutter_riverpod: ^2.5.1
@ -67,6 +68,7 @@ flutter:
- assets/images/plates/ - assets/images/plates/
- assets/images/enemies/ - assets/images/enemies/
- assets/images/backgrounds/ - assets/images/backgrounds/
- assets/audio/
# fonts: # fonts:
# - family: PixelFont # - family: PixelFont