feat: add timer to timebased Exercises
This commit is contained in:
parent
cdc5e44bb3
commit
d4be30cf74
10 changed files with 567 additions and 58 deletions
|
|
@ -123,6 +123,8 @@
|
||||||
},
|
},
|
||||||
"battleWeight": "GEWICHT",
|
"battleWeight": "GEWICHT",
|
||||||
"battleReps": "WH",
|
"battleReps": "WH",
|
||||||
|
"battleTime": "ZEIT",
|
||||||
|
"battleStartTimer": "Timer starten",
|
||||||
"battleAssistance": "UNTERSTÜTZUNG",
|
"battleAssistance": "UNTERSTÜTZUNG",
|
||||||
"battleCompleteSet": "SATZ ABSCHLIESSEN",
|
"battleCompleteSet": "SATZ ABSCHLIESSEN",
|
||||||
"battleRest": "PAUSE",
|
"battleRest": "PAUSE",
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,8 @@
|
||||||
},
|
},
|
||||||
"battleWeight": "WEIGHT",
|
"battleWeight": "WEIGHT",
|
||||||
"battleReps": "REPS",
|
"battleReps": "REPS",
|
||||||
|
"battleTime": "TIME",
|
||||||
|
"battleStartTimer": "Start Timer",
|
||||||
"battleAssistance": "ASSISTANCE",
|
"battleAssistance": "ASSISTANCE",
|
||||||
"battleCompleteSet": "COMPLETE SET",
|
"battleCompleteSet": "COMPLETE SET",
|
||||||
"battleRest": "REST",
|
"battleRest": "REST",
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|
||||||
Container(
|
Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
|
|
@ -127,7 +126,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
l10n.loginWelcomeBack,
|
l10n.loginWelcomeBack,
|
||||||
style: Theme.of(context).textTheme.displayMedium,
|
style: Theme.of(context).textTheme.displayMedium,
|
||||||
|
|
@ -140,7 +138,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
|
|
||||||
if (_errorMessage != null)
|
if (_errorMessage != null)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|
@ -173,7 +170,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
focusNode: _emailFocusNode,
|
focusNode: _emailFocusNode,
|
||||||
|
|
@ -198,8 +194,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Password Field
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
focusNode: _passwordFocusNode,
|
focusNode: _passwordFocusNode,
|
||||||
|
|
@ -234,7 +228,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _isLoading ? null : _handleLogin,
|
onPressed: _isLoading ? null : _handleLogin,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
@ -262,7 +255,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -278,7 +270,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -494,21 +494,6 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// if (cycle != null)
|
|
||||||
// Padding(
|
|
||||||
// padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
||||||
// child: Row(
|
|
||||||
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
// children: [
|
|
||||||
// _StatBox(
|
|
||||||
// label: l10n.hubCycleLabel,
|
|
||||||
// value: '#${cycle.cycleNumber}'),
|
|
||||||
// _StatBox(
|
|
||||||
// label: l10n.hubActiveLabel,
|
|
||||||
// value: l10n.hubActiveYes),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
const Spacer(flex: 1),
|
const Spacer(flex: 1),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
|
|
||||||
|
|
@ -185,9 +185,6 @@ class _LobbyScreenState extends ConsumerState<LobbyScreen> {
|
||||||
await ref
|
await ref
|
||||||
.read(partyRepositoryProvider)
|
.read(partyRepositoryProvider)
|
||||||
.startRaid(party.id, customHp: raidHp);
|
.startRaid(party.id, customHp: raidHp);
|
||||||
// ref
|
|
||||||
// .read(partyRepositoryProvider)
|
|
||||||
// .startRaid(party.id);
|
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.lobbyStartRaid,
|
l10n.lobbyStartRaid,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ class BattleState {
|
||||||
final bool isResting;
|
final bool isResting;
|
||||||
final int restSeconds;
|
final int restSeconds;
|
||||||
final String? error;
|
final String? error;
|
||||||
|
final bool isExerciseTimerActive;
|
||||||
|
final int exerciseTimerSeconds;
|
||||||
|
|
||||||
const BattleState({
|
const BattleState({
|
||||||
this.exercises = const [],
|
this.exercises = const [],
|
||||||
|
|
@ -22,6 +24,8 @@ class BattleState {
|
||||||
this.isLoading = true,
|
this.isLoading = true,
|
||||||
this.isResting = false,
|
this.isResting = false,
|
||||||
this.restSeconds = 0,
|
this.restSeconds = 0,
|
||||||
|
this.isExerciseTimerActive = false,
|
||||||
|
this.exerciseTimerSeconds = 0,
|
||||||
this.error,
|
this.error,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -33,6 +37,8 @@ class BattleState {
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
bool? isResting,
|
bool? isResting,
|
||||||
int? restSeconds,
|
int? restSeconds,
|
||||||
|
bool? isExerciseTimerActive,
|
||||||
|
int? exerciseTimerSeconds,
|
||||||
String? error,
|
String? error,
|
||||||
}) {
|
}) {
|
||||||
return BattleState(
|
return BattleState(
|
||||||
|
|
@ -43,6 +49,9 @@ class BattleState {
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
isResting: isResting ?? this.isResting,
|
isResting: isResting ?? this.isResting,
|
||||||
restSeconds: restSeconds ?? this.restSeconds,
|
restSeconds: restSeconds ?? this.restSeconds,
|
||||||
|
isExerciseTimerActive:
|
||||||
|
isExerciseTimerActive ?? this.isExerciseTimerActive,
|
||||||
|
exerciseTimerSeconds: exerciseTimerSeconds ?? this.exerciseTimerSeconds,
|
||||||
error: error ?? this.error,
|
error: error ?? this.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +102,30 @@ class BattleController extends _$BattleController {
|
||||||
state = state.copyWith(isResting: false, restSeconds: 0);
|
state = state.copyWith(isResting: false, restSeconds: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void startExerciseTimer(int seconds) {
|
||||||
|
state = state.copyWith(
|
||||||
|
isExerciseTimerActive: true, exerciseTimerSeconds: seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tickExerciseTimer() {
|
||||||
|
if (state.exerciseTimerSeconds > 0) {
|
||||||
|
state =
|
||||||
|
state.copyWith(exerciseTimerSeconds: state.exerciseTimerSeconds - 1);
|
||||||
|
} else {
|
||||||
|
state = state.copyWith(isExerciseTimerActive: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopExerciseTimer() {
|
||||||
|
state =
|
||||||
|
state.copyWith(isExerciseTimerActive: false, exerciseTimerSeconds: 0);
|
||||||
|
}
|
||||||
|
|
||||||
void completeSet({required int repsActual}) {
|
void completeSet({required int repsActual}) {
|
||||||
|
if (state.isExerciseTimerActive) {
|
||||||
|
state = state.copyWith(isExerciseTimerActive: false);
|
||||||
|
}
|
||||||
|
|
||||||
final currentExercise = state.exercises[state.currentExerciseIndex];
|
final currentExercise = state.exercises[state.currentExerciseIndex];
|
||||||
final currentSet = currentExercise.sets[state.currentSetIndex];
|
final currentSet = currentExercise.sets[state.currentSetIndex];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ class WorkoutGeneratorService {
|
||||||
accessories.add(createSimple('curl', 'Barbell Curl', 3, 10,
|
accessories.add(createSimple('curl', 'Barbell Curl', 3, 10,
|
||||||
weight: calculateWeight(pullupTm, 0.2)));
|
weight: calculateWeight(pullupTm, 0.2)));
|
||||||
|
|
||||||
accessories.add(createSimple('plank', 'Plank (30s)', 3, 1));
|
accessories.add(createSimple('plank', 'Plank (30s)', 3, 30));
|
||||||
|
|
||||||
accessories.add(_createIntervalExercise(
|
accessories.add(_createIntervalExercise(
|
||||||
id: 'kb_swing',
|
id: 'kb_swing',
|
||||||
|
|
@ -217,7 +217,7 @@ class WorkoutGeneratorService {
|
||||||
ExerciseType.scapularPull, 3, 10));
|
ExerciseType.scapularPull, 3, 10));
|
||||||
|
|
||||||
exercises.add(createAccessory(
|
exercises.add(createAccessory(
|
||||||
'plank', 'Core Plank (45s)', ExerciseType.plank, 3, 1));
|
'plank', 'Core Plank (45s)', ExerciseType.plank, 3, 45));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import 'package:slrpg_app/src/features/multiplayer/presentation/screens/lobby_sc
|
||||||
import 'package:slrpg_app/src/features/workout_runner/application/battle_controller.dart';
|
import 'package:slrpg_app/src/features/workout_runner/application/battle_controller.dart';
|
||||||
import 'package:slrpg_app/src/features/workout_runner/application/workout_completion_service.dart';
|
import 'package:slrpg_app/src/features/workout_runner/application/workout_completion_service.dart';
|
||||||
import 'package:slrpg_app/src/features/workout_runner/application/workout_loader_service.dart';
|
import 'package:slrpg_app/src/features/workout_runner/application/workout_loader_service.dart';
|
||||||
|
import 'package:slrpg_app/src/features/workout_runner/presentation/widgets/timer_widget.dart';
|
||||||
import 'package:slrpg_app/src/shared/domain/entities/exercise.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/entities/workout_set.dart';
|
||||||
|
|
||||||
|
|
@ -68,6 +69,11 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isTimeBasedExercise(String exerciseId) {
|
||||||
|
final id = exerciseId.toLowerCase();
|
||||||
|
return id.contains('plank') || id.contains('hold') || id.contains('static');
|
||||||
|
}
|
||||||
|
|
||||||
String _getEnemyAsset(String exerciseId) {
|
String _getEnemyAsset(String exerciseId) {
|
||||||
switch (exerciseId) {
|
switch (exerciseId) {
|
||||||
case 'squat':
|
case 'squat':
|
||||||
|
|
@ -560,6 +566,356 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeBasedView(
|
||||||
|
Exercise currentExercise,
|
||||||
|
WorkoutSet currentSet,
|
||||||
|
PlateLoadResult plateResult,
|
||||||
|
int completedHP,
|
||||||
|
int totalHP,
|
||||||
|
) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final state = ref.watch(battleControllerProvider);
|
||||||
|
final controller = ref.read(battleControllerProvider.notifier);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 3,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
child: Image.asset(
|
||||||
|
_getEnemyAsset(currentExercise.exerciseId),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
color: Colors.white.withValues(alpha: 0.9),
|
||||||
|
colorBlendMode: BlendMode.modulate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: Colors.white24),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
l10n.battleWave(
|
||||||
|
state.currentExerciseIndex + 1,
|
||||||
|
state.exercises.length,
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 10,
|
||||||
|
left: 32,
|
||||||
|
right: 32,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.favorite,
|
||||||
|
color: AppTheme.errorColor, size: 24),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
widget.partyId != null
|
||||||
|
? _buildMultiplayerHpBar()
|
||||||
|
: EnemyHPBar(
|
||||||
|
current: totalHP - completedHP,
|
||||||
|
max: totalHP,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 7,
|
||||||
|
child: 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(
|
||||||
|
currentExercise.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: () => _showExerciseGuide(
|
||||||
|
currentExercise.exerciseId),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
l10n.battleSet(
|
||||||
|
state.currentSetIndex + 1,
|
||||||
|
currentExercise.sets.length,
|
||||||
|
),
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
AppTheme.backgroundColor.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
AppTheme.primaryColor.withValues(alpha: 0.3),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: TimerWidget(
|
||||||
|
key: ValueKey(
|
||||||
|
'${currentExercise.exerciseId}_${state.currentExerciseIndex}_${state.currentSetIndex}',
|
||||||
|
),
|
||||||
|
durationSeconds: currentSet.repsTarget,
|
||||||
|
onComplete: () {
|
||||||
|
controller.stopExerciseTimer();
|
||||||
|
_showTimeBasedCompleteDialog(currentSet);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
if (currentSet.targetWeightTotal > 0) ...[
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surfaceColor,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.white10),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.fitness_center,
|
||||||
|
color: AppTheme.primaryColor),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'${currentSet.targetWeightTotal} kg',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
|
||||||
|
// // Reps Info (falls vorhanden)
|
||||||
|
// if (currentSet.repsTarget > 0)
|
||||||
|
// Container(
|
||||||
|
// padding: const EdgeInsets.symmetric(
|
||||||
|
// horizontal: 16,
|
||||||
|
// vertical: 12,
|
||||||
|
// ),
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// color: AppTheme.secondaryColor
|
||||||
|
// .withValues(alpha: 0.1),
|
||||||
|
// borderRadius: BorderRadius.circular(8),
|
||||||
|
// border:
|
||||||
|
// Border.all(color: AppTheme.secondaryColor),
|
||||||
|
// ),
|
||||||
|
// child: Text(
|
||||||
|
// '${currentSet.repsTarget} ${currentSet.isAmrap ? "+" : ""} Reps',
|
||||||
|
// style: const TextStyle(
|
||||||
|
// color: AppTheme.secondaryColor,
|
||||||
|
// fontSize: 18,
|
||||||
|
// fontWeight: FontWeight.bold,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
color: AppTheme.surfaceColor,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.stopExerciseTimer();
|
||||||
|
_showTimeBasedCompleteDialog(currentSet);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
backgroundColor: AppTheme.successColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
l10n.battleCompleteSet,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showTimeBasedCompleteDialog(WorkoutSet currentSet) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
if (currentSet.repsTarget > 0) {
|
||||||
|
int tempReps = currentSet.repsTarget;
|
||||||
|
|
||||||
|
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.successColor),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Time Complete!',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'How many reps did you complete?',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_CounterButton(
|
||||||
|
icon: Icons.remove,
|
||||||
|
onTap: tempReps > 0
|
||||||
|
? () => setModalState(() => tempReps--)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 120,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
'$tempReps',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 72,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_CounterButton(
|
||||||
|
icon: Icons.add,
|
||||||
|
onTap: () => setModalState(() => tempReps++),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_completeSetAndProgress(tempReps);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.successColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'CONFIRM',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_completeSetAndProgress(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildWorkoutScreen(
|
Widget _buildWorkoutScreen(
|
||||||
Exercise currentExercise,
|
Exercise currentExercise,
|
||||||
WorkoutSet currentSet,
|
WorkoutSet currentSet,
|
||||||
|
|
@ -572,6 +928,16 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
return _buildEmomView(currentExercise, currentSet, completedHP, totalHP);
|
return _buildEmomView(currentExercise, currentSet, completedHP, totalHP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_isTimeBasedExercise(currentExercise.exerciseId)) {
|
||||||
|
return _buildTimeBasedView(
|
||||||
|
currentExercise,
|
||||||
|
currentSet,
|
||||||
|
plateResult,
|
||||||
|
completedHP,
|
||||||
|
totalHP,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final state = ref.watch(battleControllerProvider);
|
final state = ref.watch(battleControllerProvider);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,35 @@ import 'dart:async';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
|
|
||||||
class TimerWidget extends StatefulWidget {
|
class TimerWidget extends StatefulWidget {
|
||||||
const TimerWidget({super.key});
|
final int durationSeconds;
|
||||||
|
final VoidCallback? onComplete;
|
||||||
|
final bool autoStart;
|
||||||
|
|
||||||
|
const TimerWidget({
|
||||||
|
super.key,
|
||||||
|
required this.durationSeconds,
|
||||||
|
this.onComplete,
|
||||||
|
this.autoStart = true,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TimerWidget> createState() => _TimerWidgetState();
|
State<TimerWidget> createState() => _TimerWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TimerWidgetState extends State<TimerWidget> {
|
class _TimerWidgetState extends State<TimerWidget> {
|
||||||
int _seconds = 0;
|
late int _secondsRemaining;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
bool _isRunning = false;
|
bool _isRunning = false;
|
||||||
|
bool _isCompleted = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_secondsRemaining = widget.durationSeconds;
|
||||||
|
if (widget.autoStart) {
|
||||||
|
_start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
|
@ -21,8 +40,19 @@ class _TimerWidgetState extends State<TimerWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _start() {
|
void _start() {
|
||||||
|
if (_isCompleted) return;
|
||||||
|
|
||||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
setState(() => _seconds++);
|
setState(() {
|
||||||
|
if (_secondsRemaining > 0) {
|
||||||
|
_secondsRemaining--;
|
||||||
|
} else {
|
||||||
|
_timer?.cancel();
|
||||||
|
_isRunning = false;
|
||||||
|
_isCompleted = true;
|
||||||
|
widget.onComplete?.call();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
setState(() => _isRunning = true);
|
setState(() => _isRunning = true);
|
||||||
}
|
}
|
||||||
|
|
@ -35,42 +65,151 @@ class _TimerWidgetState extends State<TimerWidget> {
|
||||||
void _reset() {
|
void _reset() {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
setState(() {
|
setState(() {
|
||||||
_seconds = 0;
|
_secondsRemaining = widget.durationSeconds;
|
||||||
_isRunning = false;
|
_isRunning = false;
|
||||||
|
_isCompleted = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _skip() {
|
||||||
|
_timer?.cancel();
|
||||||
|
setState(() {
|
||||||
|
_secondsRemaining = 0;
|
||||||
|
_isRunning = false;
|
||||||
|
_isCompleted = true;
|
||||||
|
});
|
||||||
|
widget.onComplete?.call();
|
||||||
|
}
|
||||||
|
|
||||||
String _formatTime() {
|
String _formatTime() {
|
||||||
final minutes = _seconds ~/ 60;
|
final minutes = _secondsRemaining ~/ 60;
|
||||||
final secs = _seconds % 60;
|
final secs = _secondsRemaining % 60;
|
||||||
return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
|
return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Color _getTimerColor() {
|
||||||
|
if (_isCompleted) return AppTheme.successColor;
|
||||||
|
if (_secondsRemaining <= 10) return AppTheme.errorColor;
|
||||||
|
if (_secondsRemaining <= 30) return Colors.orange;
|
||||||
|
return AppTheme.primaryColor;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
final progress = widget.durationSeconds > 0
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
? _secondsRemaining / widget.durationSeconds
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
strokeWidth: 12,
|
||||||
|
backgroundColor: AppTheme.xpBarBackground,
|
||||||
|
color: _getTimerColor(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_formatTime(),
|
_formatTime(),
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: Theme.of(context).textTheme.displayLarge?.copyWith(
|
||||||
color: AppTheme.primaryColor,
|
fontSize: 48,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: _getTimerColor(),
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
if (_isCompleted)
|
||||||
IconButton(
|
const Padding(
|
||||||
icon: Icon(_isRunning ? Icons.pause : Icons.play_arrow),
|
padding: EdgeInsets.only(top: 8),
|
||||||
onPressed: _isRunning ? _pause : _start,
|
child: Text(
|
||||||
color: AppTheme.primaryColor,
|
'COMPLETE!',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.successColor,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1.5,
|
||||||
),
|
),
|
||||||
IconButton(
|
),
|
||||||
icon: const Icon(Icons.refresh),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (!_isCompleted) ...[
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _isRunning ? _pause : _start,
|
||||||
|
icon: Icon(_isRunning ? Icons.pause : Icons.play_arrow),
|
||||||
|
label: Text(_isRunning ? 'PAUSE' : 'START'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor:
|
||||||
|
_isRunning ? Colors.orange : AppTheme.primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
OutlinedButton.icon(
|
||||||
onPressed: _reset,
|
onPressed: _reset,
|
||||||
color: AppTheme.primaryColor,
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('RESET'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: AppTheme.primaryColor,
|
||||||
|
side: const BorderSide(color: AppTheme.primaryColor),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _skip,
|
||||||
|
icon: const Icon(Icons.skip_next),
|
||||||
|
label: const Text('SKIP'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.grey,
|
||||||
|
side: const BorderSide(color: Colors.grey),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _reset,
|
||||||
|
icon: const Icon(Icons.replay),
|
||||||
|
label: const Text('RESTART'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 32,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,11 +104,6 @@ class ApiClient {
|
||||||
return handler.next(error);
|
return handler.next(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// onError: (error, handler) async {
|
|
||||||
// if (error.response?.statusCode == 401) {
|
|
||||||
// _logger.w('Unauthorized - clearing token');
|
|
||||||
// await _storage.delete(key: AppConstants.keyAuthToken);
|
|
||||||
// }
|
|
||||||
return handler.next(error);
|
return handler.next(error);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue