Stage 4: guided Player for both Modes and the Session summary
- buildSegments lays a Plan out as holds with Transitions (get ready, switch sides, next round, next exercise) or sets with count-up Rests; Supersets alternate members and rest per round unless asked otherwise - SessionRunner drives segments against an injected clock: auto-advance with a chime, pause, skip, back, rep sets confirmed with logged reps and kg, timed sets started on demand, early end - Player screen: countdown ring, accent pulse in the last 3 s, Flip Animation, next-up cards, Rest Screen, mute toggle, screen kept on - Sessions table (schema v2 migration) keeps a Plan snapshot and Set Logs; nothing is saved when no hold or set was done - Summary with time, exercises or sets logged, and every Set Log - Synthesised quiet cue sounds (tools/sounds), status bar per Mode Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bbdca95541
commit
971ff64117
28 files changed
+3465
-84
No files matched your search
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../design/zen_style.dart';
|
||||
import '../../exercises/exercise_library.dart';
|
||||
import '../../history/session_repository.dart';
|
||||
import '../../localization/app_localizations.dart';
|
||||
import '../../modes/mode.dart';
|
||||
import '../../plans/models/plan.dart';
|
||||
import '../../plans/plan_labels.dart';
|
||||
import '../session_runner.dart';
|
||||
|
||||
/// Shown when a Session ends: how long it took, what was done and, for
|
||||
/// Workouts, every logged set.
|
||||
class SummaryView extends StatelessWidget {
|
||||
const SummaryView({super.key, required this.sessionId});
|
||||
|
||||
static const routeName = '/summary';
|
||||
|
||||
final String sessionId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final s = ZenStyle.of(context);
|
||||
final text = Theme.of(context).textTheme;
|
||||
return Scaffold(
|
||||
body: FutureBuilder(
|
||||
future: context.read<SessionRepository>().get(sessionId),
|
||||
builder: (context, snapshot) {
|
||||
final session = snapshot.data;
|
||||
if (session == null) return const SizedBox.shrink();
|
||||
final exercises = {
|
||||
for (final item in session.items)
|
||||
...switch (item) {
|
||||
WorkoutStep(:final exerciseId) || StretchingStep(:final exerciseId) => [exerciseId],
|
||||
Superset(:final members) => members.map((m) => m.exerciseId),
|
||||
},
|
||||
};
|
||||
return SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 40, 20, 24),
|
||||
children: [
|
||||
Icon(
|
||||
session.complete ? Icons.check_circle_rounded : Icons.flag_rounded,
|
||||
size: 56,
|
||||
color: session.complete ? s.accent : s.inkMuted,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(session.complete ? l10n.summaryComplete : l10n.summaryIncomplete, style: text.headlineLarge),
|
||||
const SizedBox(height: 4),
|
||||
Text(session.planName, style: text.titleMedium?.copyWith(color: s.inkMuted)),
|
||||
const SizedBox(height: 28),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _Stat(
|
||||
label: l10n.summaryDuration,
|
||||
value: l10n.durationMinutes((session.duration.inSeconds / 60).round()),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: session.mode == Mode.workout
|
||||
? _Stat(label: l10n.summarySets, value: '${session.setLogs.length}')
|
||||
: _Stat(label: l10n.summaryExercises, value: '${exercises.length}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (session.setLogs.isNotEmpty) ...[
|
||||
const SizedBox(height: 28),
|
||||
Text(l10n.summaryLogTitle, style: text.titleMedium),
|
||||
const SizedBox(height: 10),
|
||||
for (final group in _byExercise(session.setLogs)) _LogGroup(logs: group),
|
||||
],
|
||||
const SizedBox(height: 28),
|
||||
SizedBox(
|
||||
height: 52,
|
||||
child: FilledButton(onPressed: () => Navigator.of(context).pop(), child: Text(l10n.done)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Consecutive logs of the same Exercise together, as they were done.
|
||||
static List<List<SetLog>> _byExercise(List<SetLog> logs) {
|
||||
final groups = <List<SetLog>>[];
|
||||
for (final log in logs) {
|
||||
if (groups.isNotEmpty && groups.last.first.exerciseId == log.exerciseId) {
|
||||
groups.last.add(log);
|
||||
} else {
|
||||
groups.add([log]);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
|
||||
class _Stat extends StatelessWidget {
|
||||
const _Stat({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = ZenStyle.of(context);
|
||||
final text = Theme.of(context).textTheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: s.surface, borderRadius: BorderRadius.circular(s.radius * 0.7)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: text.bodySmall?.copyWith(color: s.inkMuted)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: text.headlineSmall),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LogGroup extends StatelessWidget {
|
||||
const _LogGroup({required this.logs});
|
||||
|
||||
final List<SetLog> logs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final s = ZenStyle.of(context);
|
||||
final text = Theme.of(context).textTheme;
|
||||
final name = context.read<ExerciseLibrary>().byId(logs.first.exerciseId)?.name ?? logs.first.exerciseId;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(name, style: text.titleSmall),
|
||||
for (final log in logs)
|
||||
Text(
|
||||
[
|
||||
l10n.setLogLine(log.set),
|
||||
if (log.reps != null) l10n.repsCount(log.reps!),
|
||||
if (log.seconds != null) l10n.secondsShort(log.seconds!),
|
||||
if (log.weightKg != null) l10n.weightKg(kgNumber(log.weightKg!)),
|
||||
].join(' · '),
|
||||
style: text.bodyMedium?.copyWith(color: s.inkMuted),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user