feat(train): a workout's page as Strava's, maps readable and in the app's language, routes opening near the person (ADR 0030, D63)

- A workout with a track: its map to the foot of the screen under a sheet
  with its name; once the sheet settles, the map fits the track anew, eased,
  into what is left free; pulled up, the round buttons over the map fold
  into an app bar whose chevron lowers the sheet again. From expanded
  windows on, the map beside the rest. The page stays in Home but opens
  over the shell, without the floating bar.
- What the map's source sees moves behind a question mark.
- The map's credit on the app's theme: flutter_map's, built on
  flutter/material, never saw it (material_ui's Theme is a class of its
  own) and drew the theme's text on white; map_tiles read that Theme too,
  and so never chose the instance's dark map (D50).
- Places named in the app's language (name:<language>) where OpenFreeMap's
  styles ask for the English name.
- The route planner's hint behind a question mark; a new route's map opens
  at zoom 14 where the phone was last known to be, else where the newest
  workout with a track started; its details beside the map from expanded
  windows on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-03 22:02:06 +01:00
commit 3e90753b08
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -0,0 +1,65 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/testing.dart';
import 'package:halkyon_train_flutter/src/core/privacy/train_keys.dart';
import 'package:halkyon_train_flutter/src/feature/workout/data/workout_repository.dart';
import 'package:halkyon_train_flutter/src/feature/workout/data/workout_start.dart';
import 'package:halkyon_train_flutter/src/feature/workout/model/series_chunk.dart';
import 'package:halkyon_train_flutter/src/feature/workout/model/workout.dart';
import 'package:latlong2/latlong.dart';
import '../../support/memory_content_key_gateway.dart';
import '../../support/memory_workout_gateway.dart';
/// Where the newest workout with a track started (ADR 0030, D63): where a
/// new route's map opens when the phone can't say where it is.
void main() {
late TrainKeys keys;
late WorkoutRepository workouts;
setUp(() {
keys = TrainKeys(
MemoryContentKeyGateway(),
FakeContentCipher(),
() async => FakeAccountCrypto().keysNumbered(1),
);
workouts = WorkoutRepository(MemoryWorkoutGateway(), keys);
});
tearDown(() => keys.dispose());
Future<void> save(DateTime start, {LatLng? at}) => workouts.save(
WorkoutContent(
activity: at == null ? Activity.indoorRowing : Activity.running,
startedAt: start,
endedAt: start.add(const Duration(minutes: 20)),
),
[
if (at != null)
...chunksOf(
SeriesKind.position,
start: start,
times: const [0, 10000],
columns: {
'lat': [null, (at.latitude * 1e7).round()],
'lon': [null, (at.longitude * 1e7).round()],
},
),
],
);
test('the newest with a track, at its first fix; a newer row without one '
'passed over', () async {
await save(DateTime.utc(2026, 10, 2), at: const LatLng(47.55, 9.68));
await save(DateTime.utc(2026, 10, 20), at: const LatLng(52.52, 13.4));
await save(DateTime.utc(2026, 10, 30));
final start = await lastWorkoutStart(workouts);
expect(start!.latitude, closeTo(52.52, 1e-6));
expect(start.longitude, closeTo(13.4, 1e-6));
});
test('none without a workout with a track', () async {
await save(DateTime.utc(2026, 10, 30));
expect(await lastWorkoutStart(workouts), isNull);
});
}