Refactoring
Some checks failed
Build Linux App / build (push) Failing after 1m33s

This commit is contained in:
2026-02-23 10:02:23 -05:00
parent 21f1387fa8
commit 0c9eb8878d
57 changed files with 8179 additions and 1114 deletions

View File

@@ -5,19 +5,25 @@ import 'package:trainhub_flutter/core/constants/ui_constants.dart';
class SessionControls extends StatelessWidget {
final bool isRunning;
final bool isFinished;
final bool isTimeBased;
final VoidCallback onPause;
final VoidCallback onPlay;
final VoidCallback onNext;
final VoidCallback onPrevious;
final VoidCallback onRewind;
final VoidCallback onFastForward;
const SessionControls({
super.key,
required this.isRunning,
required this.isFinished,
required this.isTimeBased,
required this.onPause,
required this.onPlay,
required this.onNext,
required this.onPrevious,
required this.onRewind,
required this.onFastForward,
});
@override
@@ -39,13 +45,20 @@ class SessionControls extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (isTimeBased) ...[
_ControlButton(
icon: Icons.replay_10,
onTap: onRewind,
size: 24,
),
const SizedBox(width: UIConstants.spacing8),
],
_ControlButton(
icon: Icons.skip_previous_rounded,
onTap: onPrevious,
size: 28,
),
const SizedBox(width: UIConstants.spacing24),
// Play/Pause - larger main button
Container(
width: 56,
height: 56,
@@ -79,6 +92,14 @@ class SessionControls extends StatelessWidget {
onTap: onNext,
size: 28,
),
if (isTimeBased) ...[
const SizedBox(width: UIConstants.spacing8),
_ControlButton(
icon: Icons.forward_10,
onTap: onFastForward,
size: 24,
),
],
],
),
);

View File

@@ -20,12 +20,20 @@ class WorkoutSessionController extends _$WorkoutSessionController {
final activities = _buildSequence(plan);
ref.onDispose(() => _timer?.cancel());
final initialState = WorkoutSessionState(activities: activities);
if (activities.isNotEmpty) {
final first = activities.first;
return initialState.copyWith(timeRemaining: first.duration);
if (activities.isEmpty) {
return WorkoutSessionState(activities: activities);
}
final first = activities.first;
final initialState = WorkoutSessionState(
activities: activities,
timeRemaining: first.duration,
);
if (first.isTimeBased) {
Future.microtask(startTimer);
}
return initialState;
}
@@ -85,6 +93,19 @@ class WorkoutSessionController extends _$WorkoutSessionController {
}
}
void rewindSeconds(int amount) {
final currentState = state.value;
if (currentState == null) return;
final maxDuration = currentState.currentActivity?.duration ?? 0;
final newRemaining = (currentState.timeRemaining + amount).clamp(
0,
maxDuration,
);
state = AsyncValue.data(
currentState.copyWith(timeRemaining: newRemaining),
);
}
void _tick(Timer timer) {
if (state.value?.isFinished ?? true) return;
var currentState = state.value!;
@@ -98,7 +119,7 @@ class WorkoutSessionController extends _$WorkoutSessionController {
if (newState.timeRemaining > 0) {
newState = newState.copyWith(timeRemaining: newState.timeRemaining - 1);
} else {
state = AsyncValue.data(newState); // update interim state before next
state = AsyncValue.data(newState);
_goNext(newState);
return;
}
@@ -123,7 +144,7 @@ class WorkoutSessionController extends _$WorkoutSessionController {
state = AsyncValue.data(newState);
if (nextActivity.isRest) {
if (nextActivity.isTimeBased) {
startTimer();
} else {
pauseTimer();
@@ -152,23 +173,21 @@ class WorkoutSessionController extends _$WorkoutSessionController {
void jumpTo(int index) {
final currentState = state.value;
if (currentState != null &&
index >= 0 &&
index < currentState.activities.length) {
final activity = currentState.activities[index];
if (currentState == null) return;
if (index < 0 || index >= currentState.activities.length) return;
final activity = currentState.activities[index];
state = AsyncValue.data(
currentState.copyWith(
currentIndex: index,
timeRemaining: activity.duration,
),
);
state = AsyncValue.data(
currentState.copyWith(
currentIndex: index,
timeRemaining: activity.duration,
),
);
if (activity.isRest) {
startTimer();
} else {
pauseTimer();
}
if (activity.isTimeBased) {
startTimer();
} else {
pauseTimer();
}
}

View File

@@ -7,7 +7,7 @@ part of 'workout_session_controller.dart';
// **************************************************************************
String _$workoutSessionControllerHash() =>
r'd3f53d72c80963634c6edaeb44aa5b04c9ffba6d';
r'ba4c44e3bc2de98cced4eef96f8a337fd1e43665';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -1,11 +1,10 @@
import 'dart:math';
import 'dart:ui';
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
import 'package:trainhub_flutter/domain/entities/workout_activity.dart';
import 'package:trainhub_flutter/presentation/workout_session/workout_session_controller.dart';
import 'package:trainhub_flutter/presentation/workout_session/workout_session_state.dart';
import 'package:trainhub_flutter/presentation/workout_session/widgets/activity_card.dart';
@@ -55,12 +54,14 @@ class WorkoutSessionPage extends ConsumerWidget {
const SizedBox(height: UIConstants.spacing8),
Text(
'$err',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 14,
),
style: TextStyle(color: AppColors.textMuted, fontSize: 14),
textAlign: TextAlign.center,
),
const SizedBox(height: UIConstants.spacing16),
OutlinedButton(
onPressed: () => context.router.maybePop(),
child: const Text('Go Back'),
),
],
),
),
@@ -75,11 +76,8 @@ class WorkoutSessionPage extends ConsumerWidget {
);
}
final isRest = state.currentActivity?.isRest ?? false;
return _ActiveSessionView(
state: state,
isRest: isRest,
controller: controller,
);
},
@@ -88,27 +86,32 @@ class WorkoutSessionPage extends ConsumerWidget {
}
}
// ---------------------------------------------------------------------------
// Active session view (gradient background + timer + controls)
// ---------------------------------------------------------------------------
class _ActiveSessionView extends StatelessWidget {
class _ActiveSessionView extends StatefulWidget {
final WorkoutSessionState state;
final bool isRest;
final WorkoutSessionController controller;
const _ActiveSessionView({
required this.state,
required this.isRest,
required this.controller,
});
@override
State<_ActiveSessionView> createState() => _ActiveSessionViewState();
}
class _ActiveSessionViewState extends State<_ActiveSessionView> {
bool _showActivitiesList = false;
@override
Widget build(BuildContext context) {
// Compute the time progress for the circular ring.
final activity = state.currentActivity;
final activity = widget.state.currentActivity;
final isRest = activity?.isRest ?? false;
final isTimeBased = activity?.isTimeBased ?? false;
final double timeProgress;
if (activity != null && activity.duration > 0) {
timeProgress = 1.0 - (state.timeRemaining / activity.duration);
timeProgress =
1.0 - (widget.state.timeRemaining / activity.duration);
} else {
timeProgress = 0.0;
}
@@ -118,123 +121,190 @@ class _ActiveSessionView extends StatelessWidget {
: AppColors.accent.withValues(alpha: 0.06);
final ringColor = isRest ? AppColors.info : AppColors.accent;
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColors.zinc950,
accentTint,
AppColors.zinc950,
],
stops: const [0.0, 0.5, 1.0],
),
),
child: Column(
children: [
// -- Top progress bar --
SessionProgressBar(progress: state.progress),
// -- Elapsed time badge --
Padding(
padding: const EdgeInsets.only(
top: UIConstants.spacing16,
right: UIConstants.spacing24,
return Stack(
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColors.zinc950,
accentTint,
AppColors.zinc950,
],
stops: const [0.0, 0.5, 1.0],
),
child: Align(
alignment: Alignment.centerRight,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
vertical: UIConstants.spacing4,
),
decoration: BoxDecoration(
color: AppColors.zinc800.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColors.border.withValues(alpha: 0.5),
),
child: Column(
children: [
SessionProgressBar(progress: widget.state.progress),
_buildTopBar(context),
Expanded(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (activity != null)
ActivityCard(activity: activity),
const SizedBox(height: UIConstants.spacing32),
if (isTimeBased)
_CircularTimerDisplay(
timeRemaining: widget.state.timeRemaining,
progress: timeProgress,
ringColor: ringColor,
isRunning: widget.state.isRunning,
)
else
_RepsDisplay(
reps: activity?.originalExercise?.value ?? 0,
),
const SizedBox(height: UIConstants.spacing24),
if (widget.state.nextActivity != null)
_UpNextPill(
nextActivityName: widget.state.nextActivity!.name,
isNextRest: widget.state.nextActivity!.isRest,
),
],
),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.timer_outlined,
size: 14,
color: AppColors.textMuted,
),
const SizedBox(width: 4),
Text(
_formatDuration(state.totalTimeElapsed),
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 13,
fontWeight: FontWeight.w500,
fontFeatures: [FontFeature.tabularFigures()],
),
),
],
),
),
),
),
// -- Central content --
Expanded(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Activity info card
if (activity != null)
ActivityCard(activity: activity),
const SizedBox(height: UIConstants.spacing32),
// Circular progress ring + timer
_CircularTimerDisplay(
timeRemaining: state.timeRemaining,
progress: timeProgress,
ringColor: ringColor,
isRunning: state.isRunning,
isTimeBased: activity?.isTimeBased ?? false,
),
const SizedBox(height: UIConstants.spacing24),
// "Up next" pill
if (state.nextActivity != null)
_UpNextPill(
nextActivityName: state.nextActivity!.name,
isNextRest: state.nextActivity!.isRest,
),
],
),
SessionControls(
isRunning: widget.state.isRunning,
isFinished: widget.state.isFinished,
isTimeBased: isTimeBased,
onPause: widget.controller.pauseTimer,
onPlay: widget.controller.startTimer,
onNext: widget.controller.next,
onPrevious: widget.controller.previous,
onRewind: () => widget.controller.rewindSeconds(10),
onFastForward: () => widget.controller.rewindSeconds(-10),
),
const SizedBox(height: UIConstants.spacing24),
],
),
),
if (_showActivitiesList)
_ActivitiesListPanel(
activities: widget.state.activities,
currentIndex: widget.state.currentIndex,
onJumpTo: (index) {
widget.controller.jumpTo(index);
setState(() => _showActivitiesList = false);
},
onClose: () => setState(() => _showActivitiesList = false),
),
],
);
}
Widget _buildTopBar(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing8,
),
child: Row(
children: [
IconButton(
onPressed: () => _confirmExit(context),
icon: const Icon(
Icons.arrow_back_rounded,
color: AppColors.textSecondary,
),
tooltip: 'Exit workout',
),
// -- Bottom controls --
SessionControls(
isRunning: state.isRunning,
isFinished: state.isFinished,
onPause: controller.pauseTimer,
onPlay: controller.startTimer,
onNext: controller.next,
onPrevious: controller.previous,
const Spacer(),
_buildElapsedTimeBadge(),
const SizedBox(width: UIConstants.spacing8),
IconButton(
onPressed: () =>
setState(() => _showActivitiesList = !_showActivitiesList),
icon: Icon(
_showActivitiesList
? Icons.list_rounded
: Icons.format_list_bulleted_rounded,
color: _showActivitiesList
? AppColors.accent
: AppColors.textSecondary,
),
tooltip: 'Exercise list',
),
const SizedBox(height: UIConstants.spacing24),
],
),
);
}
Widget _buildElapsedTimeBadge() {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
vertical: UIConstants.spacing4,
),
decoration: BoxDecoration(
color: AppColors.zinc800.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColors.border.withValues(alpha: 0.5),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.timer_outlined,
size: 14,
color: AppColors.textMuted,
),
const SizedBox(width: 4),
Text(
_formatDuration(widget.state.totalTimeElapsed),
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 13,
fontWeight: FontWeight.w500,
fontFeatures: [FontFeature.tabularFigures()],
),
),
],
),
);
}
Future<void> _confirmExit(BuildContext context) async {
final shouldExit = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Exit Workout?'),
content: const Text(
'Your progress will not be saved. Are you sure you want to exit?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(dialogContext, true),
style: FilledButton.styleFrom(
backgroundColor: AppColors.destructive,
),
child: const Text('Exit'),
),
],
),
);
if (shouldExit == true && context.mounted) {
widget.controller.pauseTimer();
context.router.maybePop();
}
}
String _formatDuration(int seconds) {
final m = seconds ~/ 60;
final s = seconds % 60;
@@ -242,28 +312,116 @@ class _ActiveSessionView extends StatelessWidget {
}
}
// ---------------------------------------------------------------------------
// Circular timer with arc progress ring
// ---------------------------------------------------------------------------
class _RepsDisplay extends StatelessWidget {
final int reps;
const _RepsDisplay({required this.reps});
@override
Widget build(BuildContext context) {
const double size = 260;
return SizedBox(
width: size,
height: size,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: size * 0.75,
height: size * 0.75,
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppColors.accent.withValues(alpha: 0.15),
blurRadius: 60,
spreadRadius: 10,
),
],
),
),
CustomPaint(
size: const Size(size, size),
painter: TimerRingPainter(
progress: 1.0,
ringColor: AppColors.accent,
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$reps',
style: TextStyle(
color: AppColors.textPrimary,
fontSize: 72,
fontWeight: FontWeight.w200,
letterSpacing: -2,
shadows: [
Shadow(
color: AppColors.accent.withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 4),
),
],
),
),
const Text(
'REPS',
style: TextStyle(
color: AppColors.accent,
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 4,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
decoration: BoxDecoration(
color: AppColors.zinc800.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColors.accent.withValues(alpha: 0.2),
),
),
child: const Text(
'TAP NEXT WHEN DONE',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 10,
fontWeight: FontWeight.w500,
letterSpacing: 1.5,
),
),
),
],
),
],
),
);
}
}
class _CircularTimerDisplay extends StatelessWidget {
final int timeRemaining;
final double progress;
final Color ringColor;
final bool isRunning;
final bool isTimeBased;
const _CircularTimerDisplay({
required this.timeRemaining,
required this.progress,
required this.ringColor,
required this.isRunning,
required this.isTimeBased,
});
@override
Widget build(BuildContext context) {
const double size = 220;
const double strokeWidth = 6.0;
const double size = 260;
return SizedBox(
width: size,
@@ -271,49 +429,34 @@ class _CircularTimerDisplay extends StatelessWidget {
child: Stack(
alignment: Alignment.center,
children: [
// Background track
SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
value: 1.0,
strokeWidth: strokeWidth,
color: AppColors.zinc800.withValues(alpha: 0.5),
strokeCap: StrokeCap.round,
),
),
// Progress arc
if (isTimeBased)
SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
value: progress.clamp(0.0, 1.0),
strokeWidth: strokeWidth,
color: ringColor,
backgroundColor: Colors.transparent,
strokeCap: StrokeCap.round,
),
),
// Glow behind the timer text
Container(
width: size * 0.7,
height: size * 0.7,
width: size * 0.75,
height: size * 0.75,
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: ringColor.withValues(alpha: 0.08),
blurRadius: 40,
color: ringColor.withValues(alpha: isRunning ? 0.15 : 0.05),
blurRadius: 60,
spreadRadius: 10,
),
],
),
),
// Timer text
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: progress, end: progress),
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutQuart,
builder: (context, value, child) {
return CustomPaint(
size: const Size(size, size),
painter: TimerRingPainter(
progress: value,
ringColor: ringColor,
),
);
},
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -321,39 +464,49 @@ class _CircularTimerDisplay extends StatelessWidget {
_formatTime(timeRemaining),
style: TextStyle(
color: AppColors.textPrimary,
fontSize: 52,
fontWeight: FontWeight.w300,
letterSpacing: 2,
fontSize: 64,
fontWeight: FontWeight.w200,
letterSpacing: -1,
fontFeatures: const [FontFeature.tabularFigures()],
fontFamily: 'monospace',
shadows: [
Shadow(
color: ringColor.withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 4),
),
],
),
),
if (!isTimeBased)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'UNTIMED',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 2,
),
),
),
if (isTimeBased && !isRunning)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'PAUSED',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 2,
AnimatedOpacity(
opacity: isRunning ? 0.0 : 1.0,
duration: const Duration(milliseconds: 200),
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColors.zinc800.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.zinc700.withValues(alpha: 0.5),
),
),
child: Text(
'PAUSED',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 2,
),
),
),
),
),
],
),
],
@@ -368,9 +521,82 @@ class _CircularTimerDisplay extends StatelessWidget {
}
}
// ---------------------------------------------------------------------------
// "Up next" pill
// ---------------------------------------------------------------------------
class TimerRingPainter extends CustomPainter {
final double progress;
final Color ringColor;
TimerRingPainter({
required this.progress,
required this.ringColor,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = math.min(size.width, size.height) / 2;
const strokeWidth = 10.0;
// Draw background track
final trackPaint = Paint()
..color = AppColors.zinc800.withValues(alpha: 0.4)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth;
canvas.drawCircle(center, radius, trackPaint);
if (progress <= 0.0) return;
// Draw glowing shadow for the progress
final shadowPaint = Paint()
..color = ringColor.withValues(alpha: 0.5)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth * 2
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 16);
final sweepAngle = 2 * math.pi * progress;
final startAngle = -math.pi / 2;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
shadowPaint,
);
// Draw progress ring with gradient
final gradient = SweepGradient(
startAngle: 0.0,
endAngle: 2 * math.pi,
colors: [
ringColor.withValues(alpha: 0.5),
ringColor,
ringColor.withValues(alpha: 0.8),
],
transform: const GradientRotation(-math.pi / 2),
);
final progressPaint = Paint()
..shader = gradient.createShader(Rect.fromCircle(center: center, radius: radius))
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeWidth = strokeWidth;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
progressPaint,
);
}
@override
bool shouldRepaint(TimerRingPainter oldDelegate) {
return oldDelegate.progress != progress ||
oldDelegate.ringColor != ringColor;
}
}
class _UpNextPill extends StatelessWidget {
final String nextActivityName;
final bool isNextRest;
@@ -439,9 +665,180 @@ class _UpNextPill extends StatelessWidget {
}
}
// ---------------------------------------------------------------------------
// Completion screen
// ---------------------------------------------------------------------------
class _ActivitiesListPanel extends StatelessWidget {
final List<WorkoutActivityEntity> activities;
final int currentIndex;
final Function(int) onJumpTo;
final VoidCallback onClose;
const _ActivitiesListPanel({
required this.activities,
required this.currentIndex,
required this.onJumpTo,
required this.onClose,
});
@override
Widget build(BuildContext context) {
return Positioned.fill(
child: GestureDetector(
onTap: onClose,
child: ColoredBox(
color: Colors.black.withValues(alpha: 0.5),
child: Align(
alignment: Alignment.centerRight,
child: GestureDetector(
onTap: () {},
child: Container(
width: 320,
decoration: const BoxDecoration(
color: AppColors.surfaceContainer,
border: Border(
left: BorderSide(color: AppColors.border),
),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(UIConstants.spacing16),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: AppColors.border),
),
),
child: Row(
children: [
const Text(
'All Exercises',
style: TextStyle(
color: AppColors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
IconButton(
onPressed: onClose,
icon: const Icon(
Icons.close,
color: AppColors.textSecondary,
),
),
],
),
),
Expanded(
child: ListView.builder(
itemCount: activities.length,
itemBuilder: (context, index) {
final activity = activities[index];
final isCurrent = index == currentIndex;
final isRest = activity.isRest;
return InkWell(
onTap: () => onJumpTo(index),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
decoration: BoxDecoration(
color: isCurrent
? AppColors.accent.withValues(alpha: 0.12)
: null,
border: const Border(
bottom: BorderSide(
color: AppColors.border,
width: 0.5,
),
),
),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isCurrent
? AppColors.accent
: isRest
? AppColors.info
: AppColors.zinc600,
),
),
const SizedBox(width: UIConstants.spacing12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
activity.name,
style: TextStyle(
color: isCurrent
? AppColors.textPrimary
: AppColors.textSecondary,
fontSize: 13,
fontWeight: isCurrent
? FontWeight.w600
: FontWeight.normal,
),
),
if (!isRest && activity.setIndex != null)
Text(
'Set ${activity.setIndex}/${activity.totalSets} · ${activity.sectionName ?? ''}',
style: const TextStyle(
color: AppColors.textMuted,
fontSize: 11,
),
),
],
),
),
if (activity.isTimeBased)
Text(
_formatDuration(activity.duration),
style: const TextStyle(
color: AppColors.textMuted,
fontSize: 12,
fontFeatures: [
FontFeature.tabularFigures(),
],
),
)
else if (!isRest)
Text(
'${activity.originalExercise?.value ?? 0} reps',
style: const TextStyle(
color: AppColors.textMuted,
fontSize: 12,
),
),
],
),
),
);
},
),
),
],
),
),
),
),
),
),
);
}
String _formatDuration(int seconds) {
final m = seconds ~/ 60;
final s = seconds % 60;
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
}
}
class _CompletionScreen extends StatelessWidget {
final int totalTimeElapsed;
@@ -466,7 +863,6 @@ class _CompletionScreen extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Checkmark circle
Container(
width: 96,
height: 96,
@@ -491,9 +887,7 @@ class _CompletionScreen extends StatelessWidget {
size: 48,
),
),
const SizedBox(height: UIConstants.spacing24),
const Text(
'Workout Complete',
style: TextStyle(
@@ -503,9 +897,7 @@ class _CompletionScreen extends StatelessWidget {
letterSpacing: -0.5,
),
),
const SizedBox(height: UIConstants.spacing8),
Text(
'Great job! You crushed it.',
style: TextStyle(
@@ -513,10 +905,7 @@ class _CompletionScreen extends StatelessWidget {
fontSize: 15,
),
),
const SizedBox(height: UIConstants.spacing32),
// Total time card
Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing24,
@@ -554,10 +943,7 @@ class _CompletionScreen extends StatelessWidget {
],
),
),
const SizedBox(height: UIConstants.spacing32),
// Finish button
SizedBox(
width: 200,
height: 48,