Merge branch 'dev/refactor-battlescreen'
* dev/refactor-battlescreen: refactor: refactor battlescreen and split it into seperate services and controllers
This commit is contained in:
commit
ab3d0e9c15
8 changed files with 1640 additions and 1086 deletions
|
|
@ -72,10 +72,6 @@ class PartyRepository {
|
|||
await _pb.collection('parties').update(partyId, body: body);
|
||||
}
|
||||
|
||||
// Future<void> startRaid(String partyId) async {
|
||||
// await _pb.collection('parties').update(partyId, body: {'status': 'active'});
|
||||
// }
|
||||
|
||||
Stream<Party> subscribeToParty(String partyId) async* {
|
||||
await _syncAuth();
|
||||
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ class _LobbyScreenState extends ConsumerState<LobbyScreen> {
|
|||
final cycleId = activeCycle.serverId ?? activeCycle.id.toString();
|
||||
|
||||
for (int w = 1; w <= 4; w++) {
|
||||
for (int d = 1; d <= 4; d++) {
|
||||
for (int d = 1; d <= 3; d++) {
|
||||
final workout = await workoutRepo.getWorkoutByWeekDay(
|
||||
cycleId: cycleId, week: w, day: d);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,203 @@
|
|||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:slrpg_app/src/shared/domain/entities/exercise.dart';
|
||||
import 'package:slrpg_app/src/shared/domain/entities/workout_set.dart';
|
||||
|
||||
part 'battle_controller.g.dart';
|
||||
|
||||
class BattleState {
|
||||
final List<Exercise> exercises;
|
||||
final int currentExerciseIndex;
|
||||
final int currentSetIndex;
|
||||
final int repsCompleted;
|
||||
final bool isLoading;
|
||||
final bool isResting;
|
||||
final int restSeconds;
|
||||
final String? error;
|
||||
|
||||
const BattleState({
|
||||
this.exercises = const [],
|
||||
this.currentExerciseIndex = 0,
|
||||
this.currentSetIndex = 0,
|
||||
this.repsCompleted = 0,
|
||||
this.isLoading = true,
|
||||
this.isResting = false,
|
||||
this.restSeconds = 0,
|
||||
this.error,
|
||||
});
|
||||
|
||||
BattleState copyWith({
|
||||
List<Exercise>? exercises,
|
||||
int? currentExerciseIndex,
|
||||
int? currentSetIndex,
|
||||
int? repsCompleted,
|
||||
bool? isLoading,
|
||||
bool? isResting,
|
||||
int? restSeconds,
|
||||
String? error,
|
||||
}) {
|
||||
return BattleState(
|
||||
exercises: exercises ?? this.exercises,
|
||||
currentExerciseIndex: currentExerciseIndex ?? this.currentExerciseIndex,
|
||||
currentSetIndex: currentSetIndex ?? this.currentSetIndex,
|
||||
repsCompleted: repsCompleted ?? this.repsCompleted,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isResting: isResting ?? this.isResting,
|
||||
restSeconds: restSeconds ?? this.restSeconds,
|
||||
error: error ?? this.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class BattleController extends _$BattleController {
|
||||
@override
|
||||
BattleState build() {
|
||||
return const BattleState();
|
||||
}
|
||||
|
||||
void setExercises(List<Exercise> exercises) {
|
||||
state = state.copyWith(
|
||||
exercises: exercises,
|
||||
isLoading: false,
|
||||
repsCompleted: exercises.isNotEmpty && exercises.first.sets.isNotEmpty
|
||||
? exercises.first.sets.first.repsTarget
|
||||
: 0,
|
||||
);
|
||||
}
|
||||
|
||||
void setLoading(bool loading) {
|
||||
state = state.copyWith(isLoading: loading);
|
||||
}
|
||||
|
||||
void setError(String error) {
|
||||
state = state.copyWith(error: error, isLoading: false);
|
||||
}
|
||||
|
||||
void updateRepsCompleted(int reps) {
|
||||
state = state.copyWith(repsCompleted: reps);
|
||||
}
|
||||
|
||||
void startRest(int seconds) {
|
||||
state = state.copyWith(isResting: true, restSeconds: seconds);
|
||||
}
|
||||
|
||||
void tickRest() {
|
||||
if (state.restSeconds > 0) {
|
||||
state = state.copyWith(restSeconds: state.restSeconds - 1);
|
||||
} else {
|
||||
state = state.copyWith(isResting: false);
|
||||
}
|
||||
}
|
||||
|
||||
void skipRest() {
|
||||
state = state.copyWith(isResting: false, restSeconds: 0);
|
||||
}
|
||||
|
||||
void completeSet({required int repsActual}) {
|
||||
final currentExercise = state.exercises[state.currentExerciseIndex];
|
||||
final currentSet = currentExercise.sets[state.currentSetIndex];
|
||||
|
||||
final updatedSet = currentSet.copyWith(
|
||||
repsActual: repsActual,
|
||||
completed: true,
|
||||
);
|
||||
|
||||
final updatedSets = List<WorkoutSet>.from(currentExercise.sets);
|
||||
updatedSets[state.currentSetIndex] = updatedSet;
|
||||
|
||||
final updatedExercise = currentExercise.copyWith(sets: updatedSets);
|
||||
final updatedExercises = List<Exercise>.from(state.exercises);
|
||||
updatedExercises[state.currentExerciseIndex] = updatedExercise;
|
||||
|
||||
state = state.copyWith(exercises: updatedExercises);
|
||||
}
|
||||
|
||||
void moveToNextSet() {
|
||||
final currentExercise = state.exercises[state.currentExerciseIndex];
|
||||
|
||||
if (state.currentSetIndex < currentExercise.sets.length - 1) {
|
||||
final nextSet = currentExercise.sets[state.currentSetIndex + 1];
|
||||
state = state.copyWith(
|
||||
currentSetIndex: state.currentSetIndex + 1,
|
||||
repsCompleted: nextSet.repsTarget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void moveToNextExercise() {
|
||||
if (state.currentExerciseIndex < state.exercises.length - 1) {
|
||||
final nextExercise = state.exercises[state.currentExerciseIndex + 1];
|
||||
final nextReps =
|
||||
nextExercise.sets.isNotEmpty ? nextExercise.sets.first.repsTarget : 0;
|
||||
|
||||
state = state.copyWith(
|
||||
currentExerciseIndex: state.currentExerciseIndex + 1,
|
||||
currentSetIndex: 0,
|
||||
repsCompleted: nextReps,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void adjustEmomSets(int newTotalSets) {
|
||||
final currentEx = state.exercises[state.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(state.exercises);
|
||||
updatedExercises[state.currentExerciseIndex] = updatedEx;
|
||||
|
||||
state = state.copyWith(
|
||||
exercises: updatedExercises,
|
||||
currentSetIndex: newTotalSets - 1,
|
||||
repsCompleted: updatedEx.sets.last.repsTarget,
|
||||
);
|
||||
}
|
||||
|
||||
Exercise get currentExercise => state.exercises[state.currentExerciseIndex];
|
||||
WorkoutSet get currentSet => currentExercise.sets[state.currentSetIndex];
|
||||
|
||||
bool get isLastSet =>
|
||||
state.currentSetIndex >= currentExercise.sets.length - 1;
|
||||
|
||||
bool get isLastExercise =>
|
||||
state.currentExerciseIndex >= state.exercises.length - 1;
|
||||
|
||||
int get totalHP => state.exercises.fold<int>(
|
||||
0,
|
||||
(sum, ex) => sum + ex.sets.fold<int>(0, (s, set) => s + set.repsTarget),
|
||||
);
|
||||
|
||||
int get completedHP {
|
||||
int hp = 0;
|
||||
|
||||
for (int i = 0; i < state.currentExerciseIndex; i++) {
|
||||
hp += state.exercises[i].sets.fold<int>(
|
||||
0,
|
||||
(sum, set) => sum + set.repsActual,
|
||||
);
|
||||
}
|
||||
|
||||
final currentExercise = state.exercises[state.currentExerciseIndex];
|
||||
for (int i = 0; i < state.currentSetIndex; i++) {
|
||||
hp += currentExercise.sets[i].repsActual;
|
||||
}
|
||||
|
||||
return hp;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import 'dart:async';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'rest_timer_service.g.dart';
|
||||
|
||||
class RestTimerState {
|
||||
final bool isActive;
|
||||
final int remainingSeconds;
|
||||
final int totalSeconds;
|
||||
|
||||
const RestTimerState({
|
||||
required this.isActive,
|
||||
required this.remainingSeconds,
|
||||
required this.totalSeconds,
|
||||
});
|
||||
|
||||
double get progress =>
|
||||
totalSeconds > 0 ? remainingSeconds / totalSeconds : 0.0;
|
||||
|
||||
RestTimerState copyWith({
|
||||
bool? isActive,
|
||||
int? remainingSeconds,
|
||||
int? totalSeconds,
|
||||
}) {
|
||||
return RestTimerState(
|
||||
isActive: isActive ?? this.isActive,
|
||||
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
|
||||
totalSeconds: totalSeconds ?? this.totalSeconds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class RestTimer extends _$RestTimer {
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
RestTimerState build() {
|
||||
ref.onDispose(() {
|
||||
_timer?.cancel();
|
||||
});
|
||||
|
||||
return const RestTimerState(
|
||||
isActive: false,
|
||||
remainingSeconds: 0,
|
||||
totalSeconds: 0,
|
||||
);
|
||||
}
|
||||
|
||||
void start(int seconds) {
|
||||
cancel();
|
||||
|
||||
state = RestTimerState(
|
||||
isActive: true,
|
||||
remainingSeconds: seconds,
|
||||
totalSeconds: seconds,
|
||||
);
|
||||
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (state.remainingSeconds > 0) {
|
||||
state = state.copyWith(
|
||||
remainingSeconds: state.remainingSeconds - 1,
|
||||
);
|
||||
} else {
|
||||
complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void skip() {
|
||||
cancel();
|
||||
state = state.copyWith(
|
||||
isActive: false,
|
||||
remainingSeconds: 0,
|
||||
);
|
||||
}
|
||||
|
||||
void complete() {
|
||||
cancel();
|
||||
state = state.copyWith(
|
||||
isActive: false,
|
||||
remainingSeconds: 0,
|
||||
);
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:slrpg_app/src/features/gamification/application/quest_service.dart';
|
||||
import 'package:slrpg_app/src/shared/data/remote/sync_service.dart';
|
||||
import 'package:slrpg_app/src/shared/data/repositories/cycle_repository.dart';
|
||||
import 'package:slrpg_app/src/shared/data/repositories/user_repository.dart';
|
||||
import 'package:slrpg_app/src/shared/data/repositories/workout_repository.dart';
|
||||
import 'package:slrpg_app/src/shared/domain/entities/exercise.dart';
|
||||
import 'package:slrpg_app/src/shared/domain/logic/xp_calculator.dart';
|
||||
|
||||
part 'workout_completion_service.g.dart';
|
||||
|
||||
class WorkoutCompletionResult {
|
||||
final int xpEarned;
|
||||
final int? oldLevel;
|
||||
final int? newLevel;
|
||||
|
||||
const WorkoutCompletionResult({
|
||||
required this.xpEarned,
|
||||
this.oldLevel,
|
||||
this.newLevel,
|
||||
});
|
||||
|
||||
bool get hasLevelUp =>
|
||||
oldLevel != null && newLevel != null && newLevel! > oldLevel!;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
WorkoutCompletionService workoutCompletionService(Ref ref) {
|
||||
return WorkoutCompletionService(
|
||||
userRepo: ref.watch(userRepositoryProvider),
|
||||
workoutRepo: ref.watch(workoutRepositoryProvider),
|
||||
cycleRepo: ref.watch(cycleRepositoryProvider),
|
||||
questService: ref.watch(questServiceProvider),
|
||||
syncService: ref.watch(syncServiceProvider),
|
||||
);
|
||||
}
|
||||
|
||||
class WorkoutCompletionService {
|
||||
final UserRepository userRepo;
|
||||
final WorkoutRepository workoutRepo;
|
||||
final CycleRepository cycleRepo;
|
||||
final QuestService questService;
|
||||
final SyncService syncService;
|
||||
|
||||
WorkoutCompletionService({
|
||||
required this.userRepo,
|
||||
required this.workoutRepo,
|
||||
required this.cycleRepo,
|
||||
required this.questService,
|
||||
required this.syncService,
|
||||
});
|
||||
|
||||
Future<WorkoutCompletionResult> completeWorkout({
|
||||
required List<Exercise> exercises,
|
||||
required int week,
|
||||
required int day,
|
||||
int? workoutId,
|
||||
}) async {
|
||||
final xpEarned = XPCalculator.calculateWorkoutXP(exercises);
|
||||
|
||||
await userRepo.updateXP(xpEarned);
|
||||
|
||||
int? oldLevel;
|
||||
int? newLevel;
|
||||
final user = await userRepo.getLocalUser();
|
||||
if (user != null) {
|
||||
final calculatedLevel = XPCalculator.calculateLevelFromXP(user.xp);
|
||||
if (calculatedLevel > user.level) {
|
||||
oldLevel = user.level;
|
||||
newLevel = calculatedLevel;
|
||||
await userRepo.updateLevel(calculatedLevel);
|
||||
}
|
||||
}
|
||||
|
||||
await _reportQuestEvents(exercises);
|
||||
|
||||
await _saveWorkout(
|
||||
exercises: exercises,
|
||||
week: week,
|
||||
day: day,
|
||||
workoutId: workoutId,
|
||||
xpEarned: xpEarned,
|
||||
);
|
||||
|
||||
syncService.sync();
|
||||
|
||||
return WorkoutCompletionResult(
|
||||
xpEarned: xpEarned,
|
||||
oldLevel: oldLevel,
|
||||
newLevel: newLevel,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _reportQuestEvents(List<Exercise> exercises) async {
|
||||
await questService.reportEvent(QuestTrigger.workoutComplete);
|
||||
|
||||
int totalVolume = 0;
|
||||
int totalReps = 0;
|
||||
|
||||
for (var ex in exercises) {
|
||||
for (var set in ex.sets) {
|
||||
if (set.completed) {
|
||||
totalVolume += (set.targetWeightTotal * set.repsActual).round();
|
||||
totalReps += set.repsActual;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalVolume > 0) {
|
||||
await questService.reportEvent(QuestTrigger.volume, data: totalVolume);
|
||||
}
|
||||
if (totalReps > 0) {
|
||||
await questService.reportEvent(QuestTrigger.repCount, data: totalReps);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveWorkout({
|
||||
required List<Exercise> exercises,
|
||||
required int week,
|
||||
required int day,
|
||||
int? workoutId,
|
||||
required int xpEarned,
|
||||
}) async {
|
||||
final cycle = await cycleRepo.getCurrentCycle();
|
||||
final cycleIdRef = cycle?.serverId ?? cycle?.id.toString() ?? '';
|
||||
|
||||
if (workoutId != null) {
|
||||
var workout = await workoutRepo.getWorkoutByWeekDay(
|
||||
cycleId: cycleIdRef,
|
||||
week: week,
|
||||
day: day,
|
||||
);
|
||||
|
||||
if (workout != null) {
|
||||
final updatedExercises = exercises.map((e) => e.toJson()).toList();
|
||||
final updatedWorkout = workout.copyWith(exercises: updatedExercises);
|
||||
await workoutRepo.completeWorkout(updatedWorkout, xpEarned: xpEarned);
|
||||
}
|
||||
} else {
|
||||
final exercisesJson = exercises.map((e) => e.toJson()).toList();
|
||||
await workoutRepo.createCompletedWorkout(
|
||||
cycleId: cycleIdRef,
|
||||
week: week,
|
||||
day: day,
|
||||
exercises: exercisesJson,
|
||||
xpEarned: xpEarned,
|
||||
durationSeconds: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:slrpg_app/src/shared/data/local/app_database.dart';
|
||||
import 'package:slrpg_app/src/shared/data/repositories/cycle_repository.dart';
|
||||
import 'package:slrpg_app/src/shared/data/repositories/user_repository.dart';
|
||||
import 'package:slrpg_app/src/shared/data/repositories/workout_repository.dart';
|
||||
import 'package:slrpg_app/src/shared/domain/entities/exercise.dart';
|
||||
import 'package:slrpg_app/src/shared/domain/entities/workout_set.dart';
|
||||
import 'package:slrpg_app/src/shared/domain/logic/wendler_calculator.dart';
|
||||
|
||||
final workoutLoaderServiceProvider =
|
||||
Provider((ref) => WorkoutLoaderService(ref));
|
||||
|
||||
class WorkoutLoaderService {
|
||||
final Ref _ref;
|
||||
|
||||
WorkoutLoaderService(this._ref);
|
||||
|
||||
Future<List<Exercise>> loadWorkout({
|
||||
required int week,
|
||||
required int day,
|
||||
int? workoutId,
|
||||
}) async {
|
||||
final userRepo = _ref.read(userRepositoryProvider);
|
||||
final workoutRepo = _ref.read(workoutRepositoryProvider);
|
||||
final cycleRepo = _ref.read(cycleRepositoryProvider);
|
||||
|
||||
final user = await userRepo.getLocalUser();
|
||||
if (user == null) throw Exception('User not found');
|
||||
|
||||
List<Exercise> exercises = [];
|
||||
|
||||
if (workoutId != null) {
|
||||
try {
|
||||
final allWorkouts = await workoutRepo.getAllWorkouts();
|
||||
final loaded = allWorkouts.where((w) => w.id == workoutId).firstOrNull;
|
||||
if (loaded != null && loaded.exercises.isNotEmpty) {
|
||||
exercises =
|
||||
loaded.exercises.map((e) => Exercise.fromJson(e)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback: Generieren
|
||||
}
|
||||
}
|
||||
|
||||
if (exercises.isEmpty) {
|
||||
final tms = await cycleRepo.getCurrentTrainingMaxesAsync();
|
||||
exercises = _generateExercises(week, day, user, tms);
|
||||
}
|
||||
|
||||
if (exercises.isEmpty) {
|
||||
throw Exception('No exercises configured');
|
||||
}
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
List<Exercise> _generateExercises(
|
||||
int week, int day, UserCollection user, Map<String, double> tms) {
|
||||
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 addEx(String slot, String defId, String defName, ExerciseType defType,
|
||||
bool isMain) {
|
||||
final (id, name, type) = resolveVariant(slot, defId, defName, defType);
|
||||
|
||||
String tmKey = id;
|
||||
if (id == 'bench') tmKey = 'dip';
|
||||
if (id == 'row') tmKey = 'pullup';
|
||||
|
||||
final tm = tms[tmKey] ?? 0.0;
|
||||
List<WorkoutSet> sets = [];
|
||||
|
||||
if (isMain) {
|
||||
sets = WendlerCalculator.generateSets(
|
||||
week: week,
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: user.currentBodyweight,
|
||||
);
|
||||
} else {
|
||||
if (week != 4) {
|
||||
sets = WendlerCalculator.generateFSLSets(
|
||||
trainingMax: tm,
|
||||
exerciseType: type,
|
||||
currentBodyweight: user.currentBodyweight,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (sets.isNotEmpty) {
|
||||
exercises.add(Exercise(
|
||||
exerciseId: id,
|
||||
exerciseName: isMain ? name : '$name (FSL)',
|
||||
bodyweightAtSession: user.currentBodyweight,
|
||||
sets: sets,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (day == 1) {
|
||||
addEx('legs', 'squat', 'Back Squat', ExerciseType.squat, true);
|
||||
addEx('pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, false);
|
||||
} else if (day == 2) {
|
||||
addEx('push', 'dip', 'Weighted Dip', ExerciseType.dip, true);
|
||||
addEx('legs', 'squat', 'Back Squat', ExerciseType.squat, false);
|
||||
} else if (day == 3) {
|
||||
addEx('pull', 'pullup', 'Weighted Pull-up', ExerciseType.pullup, true);
|
||||
addEx('push', 'dip', 'Weighted Dip', ExerciseType.dip, false);
|
||||
}
|
||||
|
||||
return exercises;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,244 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:slrpg_app/l10n/app_localizations.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../shared/domain/entities/exercise.dart';
|
||||
import '../../../../shared/domain/entities/workout_set.dart';
|
||||
import '../../../../shared/domain/logic/plate_calculator.dart';
|
||||
import '../widgets/plate_visualizer.dart';
|
||||
|
||||
class WorkoutContent extends StatelessWidget {
|
||||
final Exercise exercise;
|
||||
final WorkoutSet currentSet;
|
||||
final int currentSetIndex;
|
||||
final int totalSets;
|
||||
final PlateLoadResult plateResult;
|
||||
final VoidCallback onCompletePress;
|
||||
final VoidCallback onShowGuide;
|
||||
|
||||
const WorkoutContent({
|
||||
super.key,
|
||||
required this.exercise,
|
||||
required this.currentSet,
|
||||
required this.currentSetIndex,
|
||||
required this.totalSets,
|
||||
required this.plateResult,
|
||||
required this.onCompletePress,
|
||||
required this.onShowGuide,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surfaceColor.withValues(alpha: 0.95),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, -5),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 100),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
exercise.exerciseName,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineMedium
|
||||
?.copyWith(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline,
|
||||
color: Colors.white54),
|
||||
onPressed: onShowGuide,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
l10n.battleSet(currentSetIndex + 1, totalSets),
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_InfoBox(
|
||||
label: l10n.battleWeight,
|
||||
value: '${currentSet.targetWeightTotal} kg',
|
||||
),
|
||||
_InfoBox(
|
||||
label: l10n.battleReps,
|
||||
value:
|
||||
'${currentSet.repsTarget}${currentSet.isAmrap ? "+" : ""}',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (plateResult.bandAssistance != null)
|
||||
_BandAssistanceInfo(
|
||||
assistance: plateResult.bandAssistance!,
|
||||
l10n: l10n,
|
||||
)
|
||||
else
|
||||
PlateVisualizer(
|
||||
plateConfiguration: plateResult.plateConfiguration,
|
||||
isTwoSided: _isTwoSided(exercise.exerciseId),
|
||||
exerciseName: exercise.exerciseName,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_CompleteButton(onPressed: onCompletePress),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isTwoSided(String exerciseId) {
|
||||
return ['squat', 'row', 'bench', 'rdl', 'ohp', 'curl'].contains(exerciseId);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoBox extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const _InfoBox({required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BandAssistanceInfo extends StatelessWidget {
|
||||
final String assistance;
|
||||
final AppLocalizations l10n;
|
||||
|
||||
const _BandAssistanceInfo({
|
||||
required this.assistance,
|
||||
required this.l10n,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.primaryColor),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.help, color: AppTheme.primaryColor, size: 32),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.battleAssistance,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
assistance,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CompleteButton extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _CompleteButton({required this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Container(
|
||||
color: AppTheme.surfaceColor,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.black,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
l10n.battleCompleteSet,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue