feat(train): equipment by place and a plate calculator (ADR 0034)
The places a person trains at (home, a gym), each a sealed, synced `training_place` row with its equipment: bars and handles with their own weight, plates by weight and count, dumbbells and kettlebells, the rest by count, the person's own by name. A new place starts from what a home or a gym usually has. The place last chosen is remembered on the device, never its location. The plate calculator loads each weight both sides alike, the heaviest load not above it with the fewest plates, heavier inside, and over a series of sets picks the loads making the fewest plate moves, listing what comes off and goes on. Equipment is in Settings; strength plans are to build on it. The migration only creates the table, its space index and its cascade to the sync engine's spaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
b7b0b937fe
commit
064b911aa2
1 changed files with 24759 additions and 7 deletions
|
|
@ -14,6 +14,9 @@ import 'package:halkyon_train_flutter/src/core/privacy/train_keys.dart';
|
|||
import 'package:halkyon_train_flutter/src/feature/day/data/day_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/data/loop_lines.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/widget/today_card.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/equipment/data/chosen_place_store.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/equipment/data/place_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/equipment/model/equipment.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/health/data/health_connect.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/health/data/health_platform.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/health/data/health_store.dart';
|
||||
|
|
@ -66,6 +69,7 @@ import '../support/fake_route_files.dart';
|
|||
import '../support/fake_routing_gateway.dart';
|
||||
import '../support/memory_content_key_gateway.dart';
|
||||
import '../support/memory_day_gateway.dart';
|
||||
import '../support/memory_place_gateway.dart';
|
||||
import '../support/memory_plan_gateway.dart';
|
||||
import '../support/memory_profile_gateway.dart';
|
||||
import '../support/memory_route_gateway.dart';
|
||||
|
|
@ -104,6 +108,7 @@ void main() {
|
|||
PolarLoop? loop,
|
||||
PlanRepository? plans,
|
||||
RouteRepository? routes,
|
||||
PlaceRepository? places,
|
||||
RoutingGateway? routing,
|
||||
PlaceSearchGateway? placeSearch,
|
||||
OfflineMaps? offlineMaps,
|
||||
|
|
@ -223,6 +228,17 @@ void main() {
|
|||
() async => FakeAccountCrypto().keysNumbered(1),
|
||||
),
|
||||
),
|
||||
places:
|
||||
places ??
|
||||
PlaceRepository(
|
||||
MemoryPlaceGateway(),
|
||||
TrainKeys(
|
||||
MemoryContentKeyGateway(),
|
||||
FakeContentCipher(),
|
||||
() async => FakeAccountCrypto().keysNumbered(1),
|
||||
),
|
||||
),
|
||||
chosenPlace: ChosenPlaceStore(),
|
||||
routing: routing ?? FakeRoutingGateway(),
|
||||
placeSearch:
|
||||
placeSearch ?? FakePlaceSearchGateway(isOffered: false),
|
||||
|
|
@ -595,6 +611,118 @@ void main() {
|
|||
expect(find.text('Health Connect'), findsNothing);
|
||||
});
|
||||
|
||||
group('equipment (ADR 0034)', () {
|
||||
late TrainKeys keys;
|
||||
late PlaceRepository places;
|
||||
|
||||
setUp(() async {
|
||||
keys = TrainKeys(
|
||||
MemoryContentKeyGateway(),
|
||||
FakeContentCipher(),
|
||||
() async => FakeAccountCrypto().keysNumbered(1),
|
||||
);
|
||||
await keys.refresh();
|
||||
places = PlaceRepository(MemoryPlaceGateway(), keys);
|
||||
});
|
||||
|
||||
tearDown(() => keys.dispose());
|
||||
|
||||
Future<void> openEquipment(WidgetTester tester) async {
|
||||
await start(tester, usedBefore: true, places: places);
|
||||
await openSettings(tester);
|
||||
await tester.tap(find.text('Equipment'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('a place added from what a gym usually has lists its '
|
||||
'equipment, is kept, and is where the person trains now', (
|
||||
tester,
|
||||
) async {
|
||||
await openEquipment(tester);
|
||||
expect(find.text('No places yet'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Add a place'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('A gym'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.widgetWithText(TextField, 'Gym'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
|
||||
await tester.runAsync(pumpEventQueue);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.widgetWithText(AppBar, 'Gym'), findsOneWidget);
|
||||
expect(find.text('Where you train now'), findsOneWidget);
|
||||
expect(find.text('Trap bar'), findsOneWidget);
|
||||
expect(find.text('4 × 20 kg'), findsOneWidget, reason: 'the barbells');
|
||||
final kept = await tester.runAsync(places.all);
|
||||
expect(kept!.single.content, PlaceContent.gym('Gym'));
|
||||
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Gym'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Where you train now · 11 pieces of equipment'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("an item's own weight changed is kept as it is changed", (
|
||||
tester,
|
||||
) async {
|
||||
await tester.runAsync(() => places.save(PlaceContent.home('Home')));
|
||||
await openEquipment(tester);
|
||||
await tester.tap(find.text('Home'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Barbell'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Its own weight'),
|
||||
'15',
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Done'));
|
||||
await tester.runAsync(pumpEventQueue);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('15 kg'), findsOneWidget);
|
||||
final kept = await tester.runAsync(places.all);
|
||||
expect(kept!.single.content!.items.first.grams, 15000);
|
||||
});
|
||||
|
||||
testWidgets('the plate calculator loads each set and says what changes '
|
||||
'between them', (tester) async {
|
||||
await tester.runAsync(() => places.save(PlaceContent.home('Home')));
|
||||
await openEquipment(tester);
|
||||
await tester.tap(find.byTooltip('Plate calculator'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Weights, kg'),
|
||||
'60, 80, 41',
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Each side, from the inside: 20 kg'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Each side, from the inside: 20 kg + 10 kg'),
|
||||
findsOneWidget,
|
||||
);
|
||||
// 41 kg: 40 kg is as close as the plates come, 10 kg a side, the
|
||||
// outer 10 kg off first.
|
||||
expect(find.text('Take off 10 kg + 20 kg'), findsOneWidget);
|
||||
expect(find.text('Put on 20 kg'), findsOneWidget);
|
||||
expect(find.text('Put on 10 kg'), findsNWidgets(2));
|
||||
expect(find.text('5 plates move on each side'), findsOneWidget);
|
||||
expect(
|
||||
find.text(
|
||||
'1 kg below what you asked for: the plates make no closer weight',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('your body (ADR 0030, D33)', () {
|
||||
late TrainKeys keys;
|
||||
late ProfileRepository profile;
|
||||
|
|
|
|||
Loading…
Reference in a new issue