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
|
// 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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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/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,
|
||||||
|
|
|
||||||
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],
|
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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
56
pubspec.lock
56
pubspec.lock
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue