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:
CedricandClaude Opus 5.5 committed 2026-10-29 00:20:16 +01:00
1 parent bbdca95541
commit 971ff64117
28 files changed
+3465 -84

No files matched your search

+18
View File
@@ -6,7 +6,9 @@ import 'package:zenxercise/src/app.dart';
import 'package:zenxercise/src/exercises/exercise.dart';
import 'package:zenxercise/src/exercises/exercise_library.dart';
import 'package:zenxercise/src/modes/mode.dart';
import 'package:zenxercise/src/history/session_repository.dart';
import 'package:zenxercise/src/plans/plan_repository.dart';
import 'package:zenxercise/src/player/player_effects.dart';
import 'package:zenxercise/src/settings/settings_controller.dart';
import 'test_database.dart';
@@ -48,6 +50,18 @@ final sampleLibrary = ExerciseLibrary([
exercise('Barbell_Squat', Mode.workout, primary: const ['quadriceps'], equipment: 'barbell'),
]);
/// Counts chimes instead of playing them and leaves the screen alone.
class FakePlayerEffects implements PlayerEffects {
final chimes = <Mode>[];
bool screenOn = false;
@override
Future<void> chime(Mode mode) async => chimes.add(mode);
@override
Future<void> keepScreenOn(bool on) async => screenOn = on;
}
/// A Pixel-sized screen instead of the default 800×600, so the floating
/// navigation bar does not cover what a test taps.
void usePhoneScreen(WidgetTester tester) {
@@ -62,6 +76,8 @@ Future<SettingsController> pumpApp(
ExerciseLibrary? library,
Map<String, Object> prefs = const {},
PlanRepository? plans,
SessionRepository? sessions,
PlayerEffects? effects,
}) async {
SharedPreferences.setMockInitialValues(prefs);
final settings = SettingsController(SettingsService());
@@ -74,6 +90,8 @@ Future<SettingsController> pumpApp(
ChangeNotifierProvider.value(value: settings),
Provider.value(value: library ?? sampleLibrary),
Provider.value(value: plans ?? PlanRepository(db)),
Provider.value(value: sessions ?? SessionRepository(db)),
Provider<PlayerEffects>.value(value: effects ?? FakePlayerEffects()),
],
child: const ZenXerciseApp(),
),