refactor: refactor battlescreen and split it into seperate services and controllers
This commit is contained in:
parent
ce6c479e92
commit
92ab969831
8 changed files with 1640 additions and 1086 deletions
|
|
@ -72,10 +72,6 @@ class PartyRepository {
|
||||||
await _pb.collection('parties').update(partyId, body: body);
|
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* {
|
Stream<Party> subscribeToParty(String partyId) async* {
|
||||||
await _syncAuth();
|
await _syncAuth();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ class _LobbyScreenState extends ConsumerState<LobbyScreen> {
|
||||||
final cycleId = activeCycle.serverId ?? activeCycle.id.toString();
|
final cycleId = activeCycle.serverId ?? activeCycle.id.toString();
|
||||||
|
|
||||||
for (int w = 1; w <= 4; w++) {
|
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(
|
final workout = await workoutRepo.getWorkoutByWeekDay(
|
||||||
cycleId: cycleId, week: w, day: d);
|
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