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",
|
||||
"battleReps": "WH",
|
||||
"battleTime": "ZEIT",
|
||||
"battleStartTimer": "Timer starten",
|
||||
"battleAssistance": "UNTERSTÜTZUNG",
|
||||
"battleCompleteSet": "SATZ ABSCHLIESSEN",
|
||||
"battleRest": "PAUSE",
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"battleWeight": "WEIGHT",
|
||||
"battleReps": "REPS",
|
||||
"battleTime": "TIME",
|
||||
"battleStartTimer": "Start Timer",
|
||||
"battleAssistance": "ASSISTANCE",
|
||||
"battleCompleteSet": "COMPLETE SET",
|
||||
"battleRest": "REST",
|
||||
|
|
|
|||
|
|
@ -104,7 +104,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Spacer(),
|
||||
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
|
|
@ -127,7 +126,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Text(
|
||||
l10n.loginWelcomeBack,
|
||||
style: Theme.of(context).textTheme.displayMedium,
|
||||
|
|
@ -140,7 +138,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
if (_errorMessage != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
|
@ -173,7 +170,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
],
|
||||
),
|
||||
),
|
||||
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
focusNode: _emailFocusNode,
|
||||
|
|
@ -198,8 +194,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Password Field
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
focusNode: _passwordFocusNode,
|
||||
|
|
@ -234,7 +228,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleLogin,
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
|
@ -262,7 +255,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
|
@ -278,7 +270,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -494,21 +494,6 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
),
|
||||
),
|
||||
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),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
|
|
|
|||
|
|
@ -185,9 +185,6 @@ class _LobbyScreenState extends ConsumerState<LobbyScreen> {
|
|||
await ref
|
||||
.read(partyRepositoryProvider)
|
||||
.startRaid(party.id, customHp: raidHp);
|
||||
// ref
|
||||
// .read(partyRepositoryProvider)
|
||||
// .startRaid(party.id);
|
||||
},
|
||||
child: Text(
|
||||
l10n.lobbyStartRaid,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ class BattleState {
|
|||
final bool isResting;
|
||||
final int restSeconds;
|
||||
final String? error;
|
||||
final bool isExerciseTimerActive;
|
||||
final int exerciseTimerSeconds;
|
||||
|
||||
const BattleState({
|
||||
this.exercises = const [],
|
||||
|
|
@ -22,6 +24,8 @@ class BattleState {
|
|||
this.isLoading = true,
|
||||
this.isResting = false,
|
||||
this.restSeconds = 0,
|
||||
this.isExerciseTimerActive = false,
|
||||
this.exerciseTimerSeconds = 0,
|
||||
this.error,
|
||||
});
|
||||
|
||||
|
|
@ -33,6 +37,8 @@ class BattleState {
|
|||
bool? isLoading,
|
||||
bool? isResting,
|
||||
int? restSeconds,
|
||||
bool? isExerciseTimerActive,
|
||||
int? exerciseTimerSeconds,
|
||||
String? error,
|
||||
}) {
|
||||
return BattleState(
|
||||
|
|
@ -43,6 +49,9 @@ class BattleState {
|
|||
isLoading: isLoading ?? this.isLoading,
|
||||
isResting: isResting ?? this.isResting,
|
||||
restSeconds: restSeconds ?? this.restSeconds,
|
||||
isExerciseTimerActive:
|
||||
isExerciseTimerActive ?? this.isExerciseTimerActive,
|
||||
exerciseTimerSeconds: exerciseTimerSeconds ?? this.exerciseTimerSeconds,
|
||||
error: error ?? this.error,
|
||||
);
|
||||
}
|
||||
|
|
@ -93,7 +102,30 @@ class BattleController extends _$BattleController {
|
|||
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}) {
|
||||
if (state.isExerciseTimerActive) {
|
||||
state = state.copyWith(isExerciseTimerActive: false);
|
||||
}
|
||||
|
||||
final currentExercise = state.exercises[state.currentExerciseIndex];
|
||||
final currentSet = currentExercise.sets[state.currentSetIndex];
|
||||
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ class WorkoutGeneratorService {
|
|||
accessories.add(createSimple('curl', 'Barbell Curl', 3, 10,
|
||||
weight: calculateWeight(pullupTm, 0.2)));
|
||||
|
||||
accessories.add(createSimple('plank', 'Plank (30s)', 3, 1));
|
||||
accessories.add(createSimple('plank', 'Plank (30s)', 3, 30));
|
||||
|
||||
accessories.add(_createIntervalExercise(
|
||||
id: 'kb_swing',
|
||||
|
|
@ -217,7 +217,7 @@ class WorkoutGeneratorService {
|
|||
ExerciseType.scapularPull, 3, 10));
|
||||
|
||||
exercises.add(createAccessory(
|
||||
'plank', 'Core Plank (45s)', ExerciseType.plank, 3, 1));
|
||||
'plank', 'Core Plank (45s)', ExerciseType.plank, 3, 45));
|
||||
break;
|
||||
|
||||
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/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/presentation/widgets/timer_widget.dart';
|
||||
import 'package:slrpg_app/src/shared/domain/entities/exercise.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) {
|
||||
switch (exerciseId) {
|
||||
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(
|
||||
Exercise currentExercise,
|
||||
WorkoutSet currentSet,
|
||||
|
|
@ -572,6 +928,16 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
return _buildEmomView(currentExercise, currentSet, completedHP, totalHP);
|
||||
}
|
||||
|
||||
if (_isTimeBasedExercise(currentExercise.exerciseId)) {
|
||||
return _buildTimeBasedView(
|
||||
currentExercise,
|
||||
currentSet,
|
||||
plateResult,
|
||||
completedHP,
|
||||
totalHP,
|
||||
);
|
||||
}
|
||||
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final state = ref.watch(battleControllerProvider);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,35 @@ import 'dart:async';
|
|||
import '../../../../core/theme/app_theme.dart';
|
||||
|
||||
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
|
||||
State<TimerWidget> createState() => _TimerWidgetState();
|
||||
}
|
||||
|
||||
class _TimerWidgetState extends State<TimerWidget> {
|
||||
int _seconds = 0;
|
||||
late int _secondsRemaining;
|
||||
Timer? _timer;
|
||||
bool _isRunning = false;
|
||||
bool _isCompleted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_secondsRemaining = widget.durationSeconds;
|
||||
if (widget.autoStart) {
|
||||
_start();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
|
@ -21,8 +40,19 @@ class _TimerWidgetState extends State<TimerWidget> {
|
|||
}
|
||||
|
||||
void _start() {
|
||||
if (_isCompleted) return;
|
||||
|
||||
_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);
|
||||
}
|
||||
|
|
@ -35,42 +65,151 @@ class _TimerWidgetState extends State<TimerWidget> {
|
|||
void _reset() {
|
||||
_timer?.cancel();
|
||||
setState(() {
|
||||
_seconds = 0;
|
||||
_secondsRemaining = widget.durationSeconds;
|
||||
_isRunning = false;
|
||||
_isCompleted = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _skip() {
|
||||
_timer?.cancel();
|
||||
setState(() {
|
||||
_secondsRemaining = 0;
|
||||
_isRunning = false;
|
||||
_isCompleted = true;
|
||||
});
|
||||
widget.onComplete?.call();
|
||||
}
|
||||
|
||||
String _formatTime() {
|
||||
final minutes = _seconds ~/ 60;
|
||||
final secs = _seconds % 60;
|
||||
final minutes = _secondsRemaining ~/ 60;
|
||||
final secs = _secondsRemaining % 60;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
final progress = widget.durationSeconds > 0
|
||||
? _secondsRemaining / widget.durationSeconds
|
||||
: 0.0;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_formatTime(),
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: AppTheme.primaryColor,
|
||||
fontFamily: 'monospace',
|
||||
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: [
|
||||
Text(
|
||||
_formatTime(),
|
||||
style: Theme.of(context).textTheme.displayLarge?.copyWith(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getTimerColor(),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
if (_isCompleted)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
'COMPLETE!',
|
||||
style: TextStyle(
|
||||
color: AppTheme.successColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
IconButton(
|
||||
icon: Icon(_isRunning ? Icons.pause : Icons.play_arrow),
|
||||
onPressed: _isRunning ? _pause : _start,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _reset,
|
||||
color: AppTheme.primaryColor,
|
||||
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,
|
||||
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);
|
||||
}
|
||||
}
|
||||
// onError: (error, handler) async {
|
||||
// if (error.response?.statusCode == 401) {
|
||||
// _logger.w('Unauthorized - clearing token');
|
||||
// await _storage.delete(key: AppConstants.keyAuthToken);
|
||||
// }
|
||||
return handler.next(error);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue