feat(train): each km announced on foot, each 5 km by bike (ADR 0030, D49)

Step 11 takes up what OpenTracks does that Train didn't (D49). 11a: a
recording announces each kilometre on foot and each 5 km by bike, never
on a rower, where the Record sheet's switch is on for the activity
(remembered in the person's announce.json, off at first): the distance,
the moving time, the split's pace or speed and the heart rate. The app
hands the recorder the words in its language; Announcer.kt counts the
moving time and the distance, Cues.kt speaks the line after a cue still
speaking.

Not tried on a phone yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-29 14:25:13 +01:00
commit db7e0b7bcd
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -28,6 +28,7 @@ import 'package:halkyon_train_flutter/src/feature/plan/model/stored_plan.dart';
import 'package:halkyon_train_flutter/src/feature/profile/data/profile_repository.dart';
import 'package:halkyon_train_flutter/src/feature/profile/model/profile.dart';
import 'package:halkyon_train_flutter/src/feature/profile/widget/profile_screen.dart';
import 'package:halkyon_train_flutter/src/feature/recording/data/announce_store.dart';
import 'package:halkyon_train_flutter/src/feature/recording/data/device_store.dart';
import 'package:halkyon_train_flutter/src/feature/recording/data/recorded_workout.dart';
import 'package:halkyon_train_flutter/src/feature/recording/data/recorder_platform.dart';
@ -1110,6 +1111,7 @@ void main() {
file: file(),
straps: DeviceStore(File('${folder.path}/strap.json')),
rowers: DeviceStore(File('${folder.path}/rower.json')),
announces: AnnounceStore(File('${folder.path}/announce.json')),
),
workouts: workouts,
loop: loop,
@ -1165,6 +1167,51 @@ void main() {
});
});
testWidgets('announcements are switched for each activity and '
'remembered, a row offered none (D49)', (tester) async {
await withClock(Clock(() => at), () async {
await startRecording(tester);
await tester.tap(find.text('Record'));
await tester.pumpAndSettle();
await tester.tap(find.text('Cycling'));
await tester.pump();
expect(
find.text('Every 5 km: time, speed and heart rate'),
findsOneWidget,
);
await tester.tap(find.text('Announcements'));
await tester.pump();
await tester.tap(find.text('Running'));
await tester.pump();
expect(
find.text('Every kilometre: time, pace and heart rate'),
findsOneWidget,
);
expect(
tester
.widget<SwitchListTile>(
find.widgetWithText(SwitchListTile, 'Announcements'),
)
.value,
isFalse,
);
await tester.tap(find.text('Rowing indoors'));
await tester.pump();
expect(find.text('Announcements'), findsNothing);
await tester.tap(find.text('Cycling'));
await tester.pump();
await tester.tap(find.text('Start'));
await tester.runAsync(pumpEventQueue);
await pass(tester, 2);
expect(platform.started.single.announce?['every'], 5000);
expect(
AnnounceStore(File('${folder.path}/announce.json')).read(),
{Activity.cycling},
);
});
});
testWidgets('a ride is chosen, waits for GPS, shows its distance, stops '
'only held down and is saved', (tester) async {
var now = at;
@ -1451,6 +1498,8 @@ void main() {
await tester.pumpAndSettle();
await tester.tap(find.text('Running'));
await tester.pump();
await tester.ensureVisible(find.text('Route'));
await tester.pump();
await tester.tap(find.text('Route'));
await tester.runAsync(pumpEventQueue);
await tester.pumpAndSettle();
@ -2007,6 +2056,9 @@ void main() {
file: File('${folder.path}/recording.txt'),
straps: DeviceStore(File('${folder.path}/strap.json')),
rowers: DeviceStore(File('${folder.path}/rower.json')),
announces: AnnounceStore(
File('${folder.path}/announce.json'),
),
),
);
await tester.tap(find.text('Routes'));