feat(train): Settings over the tabs from a cog on Home and Profile, the account near its foot (ADR 0030, D60)

As Learn's: a cog atop Home and Profile, with the count of accounts
waiting for whoever operates the instance, opens Settings over the tabs,
where an account sheet had held the account and the training's screens.
Grouped in cards: the operator's card, the training (your body, Health
Connect, the Polar Loop, music, those the device has), the account near
the foot, About Halkyon Train last, and the version small at the very
bottom.

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-31 00:16:01 +01:00
commit 25191055ae
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
15 changed files with 322 additions and 150 deletions

File diff suppressed because one or more lines are too long

View file

@ -12,7 +12,7 @@
"accountTitle": "Konto",
"accountDeleteMessage": "Dein Konto wird auf dem Server und auf all deinen Geräten gelöscht. Dies kann nicht rückgängig gemacht werden.",
"aboutTitle": "Über",
"aboutTitle": "Über Halkyon Train",
"homeRecordHint": "Zeichne eins mit dem Knopf unten auf.",
"recordAction": "Aufzeichnen",
@ -563,5 +563,8 @@
"shellTabGroups": "Gruppen",
"shellTabProfile": "Profil",
"groupsLaterTitle": "Gruppen kommen später",
"groupsLaterHint": "Mit anderen trainieren, in euren eigenen Gruppen, kommt in einer späteren Version von Train."
"groupsLaterHint": "Mit anderen trainieren, in euren eigenen Gruppen, kommt in einer späteren Version von Train.",
"settingsTitle": "Einstellungen",
"settingsTraining": "Training",
"settingsVersion": "Version {version} (Build {build})"
}

View file

@ -14,7 +14,7 @@
"accountTitle": "Account",
"accountDeleteMessage": "Your account is deleted on the server and on all your devices. This can't be undone.",
"aboutTitle": "About",
"aboutTitle": "About Halkyon Train",
"homeRecordHint": "Record one with the button below.",
"recordAction": "Record",
@ -710,5 +710,9 @@
"shellTabGroups": "Groups",
"shellTabProfile": "Profile",
"groupsLaterTitle": "Groups come later",
"groupsLaterHint": "Training with others, in groups of your own, comes in a later version of Train."
"groupsLaterHint": "Training with others, in groups of your own, comes in a later version of Train.",
"settingsTitle": "Settings",
"settingsTraining": "Training",
"settingsVersion": "Version {version} (build {build})",
"@settingsVersion": {"placeholders": {"version": {"type": "String"}, "build": {"type": "String"}}}
}

View file

@ -145,7 +145,7 @@ abstract class AppLocalizations {
/// No description provided for @aboutTitle.
///
/// In en, this message translates to:
/// **'About'**
/// **'About Halkyon Train'**
String get aboutTitle;
/// No description provided for @homeRecordHint.
@ -3405,6 +3405,24 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Training with others, in groups of your own, comes in a later version of Train.'**
String get groupsLaterHint;
/// No description provided for @settingsTitle.
///
/// In en, this message translates to:
/// **'Settings'**
String get settingsTitle;
/// No description provided for @settingsTraining.
///
/// In en, this message translates to:
/// **'Training'**
String get settingsTraining;
/// No description provided for @settingsVersion.
///
/// In en, this message translates to:
/// **'Version {version} (build {build})'**
String settingsVersion(String version, String build);
}
class _AppLocalizationsDelegate

View file

@ -34,7 +34,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Dein Konto wird auf dem Server und auf all deinen Geräten gelöscht. Dies kann nicht rückgängig gemacht werden.';
@override
String get aboutTitle => 'Über';
String get aboutTitle => 'Über Halkyon Train';
@override
String get homeRecordHint => 'Zeichne eins mit dem Knopf unten auf.';
@ -2041,4 +2041,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get groupsLaterHint =>
'Mit anderen trainieren, in euren eigenen Gruppen, kommt in einer späteren Version von Train.';
@override
String get settingsTitle => 'Einstellungen';
@override
String get settingsTraining => 'Training';
@override
String settingsVersion(String version, String build) {
return 'Version $version (Build $build)';
}
}

View file

@ -33,7 +33,7 @@ class AppLocalizationsEn extends AppLocalizations {
'Your account is deleted on the server and on all your devices. This can\'t be undone.';
@override
String get aboutTitle => 'About';
String get aboutTitle => 'About Halkyon Train';
@override
String get homeRecordHint => 'Record one with the button below.';
@ -2029,6 +2029,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get groupsLaterHint =>
'Training with others, in groups of your own, comes in a later version of Train.';
@override
String get settingsTitle => 'Settings';
@override
String get settingsTraining => 'Training';
@override
String settingsVersion(String version, String build) {
return 'Version $version (build $build)';
}
}
/// The translations for English, as used in the United Kingdom (`en_GB`).

View file

@ -1,97 +0,0 @@
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
import 'package:halkyon_train_flutter/src/app/train_frame.dart';
import 'package:halkyon_train_flutter/src/feature/health/widget/health_screen.dart';
import 'package:halkyon_train_flutter/src/feature/loop/widget/loop_screen.dart';
import 'package:halkyon_train_flutter/src/feature/music/widget/music_screen.dart';
import 'package:halkyon_train_flutter/src/feature/profile/widget/profile_screen.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// The account: halkyon_app's sections (whose it is, on which instance,
/// switching to the account on another, signing out and deleting it;
/// SimpleLogin; its password and authenticators) in Train's words where only
/// Train can say it; and About.
Future<void> showAccountSheet(BuildContext context) => showAdaptiveSheet(
context,
builder: (context) => const _AccountSheet(),
);
class const _AccountSheet() extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return SheetScaffold(
title: l10n.accountTitle,
// What failed (signing out, deleting the account, reading it) shows
// on the sheet, not behind its barrier.
hasSnackBars: true,
body: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AccountTiles(deleteMessage: l10n.accountDeleteMessage),
// Accounts waiting for approval, for whoever operates the
// instance alone (Halkyon Learn's ADR 0021, D34).
const InstanceOperatorTiles(),
// SimpleLogin keeps the account where the instance offers it
// (ADR 0020, D21), and a password and an authenticator, signing
// in on other devices and in the instance's other apps (ADR 0017).
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [SimpleLoginSection(), AccountSecuritySection()],
),
),
// The figures of the person's body (ADR 0030, D33), here until
// Train has settings.
ListTile(
leading: const Icon(LucideIcons.personStanding),
title: Text(l10n.profileTitle),
subtitle: Text(l10n.profileHint),
onTap: () => openProfileScreen(context),
),
// Health Connect on this phone (ADR 0030, D31), here until Train
// has settings.
if (PersonScope.of<TrainPerson>(context).health != null)
ListTile(
leading: const Icon(LucideIcons.heartPulse),
title: Text(l10n.healthTitle),
subtitle: Text(l10n.healthHint),
onTap: () => openHealthScreen(context),
),
// The person's Polar Loop on this phone (D34).
if (PersonScope.of<TrainPerson>(context).loop != null)
ListTile(
leading: const Icon(LucideIcons.watch),
title: Text(l10n.loopTitle),
subtitle: Text(l10n.loopHint),
onTap: () => openLoopScreen(context),
),
// Music from Jellyfin on this phone (D47).
if (PersonScope.of<TrainPerson>(context).music != null)
ListTile(
leading: const Icon(LucideIcons.music),
title: Text(l10n.musicTitle),
subtitle: Text(l10n.musicHint),
onTap: () => openMusicScreen(context),
),
ListTile(
leading: const Icon(LucideIcons.info),
title: Text(l10n.aboutTitle),
// halkyon_app's About: the app's name and icon, the source of
// what the instance runs, which both licences ask to offer, and
// the licence list.
onTap: () => showAppAbout(
context,
icon: const Image(image: trainIcon, width: 48, height: 48),
),
),
],
),
);
}
}

View file

@ -200,6 +200,10 @@ class TrainDependencies({
/// Opens a signed-in person's dependencies ([openPerson]).
required final OpenTrainPerson _openPerson,
/// This build's version and number, which Settings shows at its foot so
/// that a report can name the release; none in tests that don't say.
final ({String version, int? build}) appVersion = (version: '', build: null),
final Future<void> Function()? _onDispose,
}) implements AppDependencies<TrainPerson> {
/// The dependencies above [context] ([AppScope]).
@ -291,6 +295,7 @@ class TrainDependencies({
assistants: assistants,
assistantOperators: assistantOperators,
fileSaver: PlatformFileSaver(),
appVersion: (version: device.appVersion, build: device.appBuild),
sourceCodeUrl: () async =>
(await client.modules.base.instance.basics()).sourceCodeUrl,
openPerson: (person) async {

View file

@ -2,7 +2,7 @@ import 'package:halkyon_train_flutter/src/feature/day/model/day.dart';
/// The app's locations (ADR 0030, D60): the five tabs, a workout's page in
/// Home and a day's in Profile, and the editors and the recording over the
/// tabs.
/// tabs, and Settings.
abstract final class TrainPaths() {
/// Home, where the app opens: the workouts.
static const home = '/';
@ -34,4 +34,7 @@ abstract final class TrainPaths() {
/// The recording under way or stopped, over the tabs.
static const recording = '/recording';
/// Settings, over the tabs.
static const settings = '/settings';
}

View file

@ -8,6 +8,7 @@ import 'package:halkyon_train_flutter/src/feature/recording/widget/recording_scr
import 'package:halkyon_train_flutter/src/feature/route/model/route.dart';
import 'package:halkyon_train_flutter/src/feature/route/widget/route_screen.dart';
import 'package:halkyon_train_flutter/src/feature/workout/widget/workout_screen.dart';
import 'package:halkyon_train_flutter/src/settings/settings_screen.dart';
import 'package:halkyon_train_flutter/src/tabs/groups_tab.dart';
import 'package:halkyon_train_flutter/src/tabs/home_tab.dart';
import 'package:halkyon_train_flutter/src/tabs/maps_tab.dart';
@ -109,6 +110,11 @@ GoRouter buildTrainRouter() => GoRouter(
path: TrainPaths.recording,
builder: (context, state) => const RecordingScreen(),
),
// In the frame without the rail from medium windows on, as Learn's.
GoRoute(
path: TrainPaths.settings,
builder: (context, state) => const FramedScreen(child: SettingsScreen()),
),
],
);

View file

@ -0,0 +1,181 @@
import 'package:go_router/go_router.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
import 'package:halkyon_train_flutter/src/app/train_frame.dart';
import 'package:halkyon_train_flutter/src/app/train_paths.dart';
import 'package:halkyon_train_flutter/src/feature/health/widget/health_screen.dart';
import 'package:halkyon_train_flutter/src/feature/loop/widget/loop_screen.dart';
import 'package:halkyon_train_flutter/src/feature/music/widget/music_screen.dart';
import 'package:halkyon_train_flutter/src/feature/profile/widget/profile_screen.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// The cog to [SettingsScreen], atop Home and Profile as Learn's, with the
/// count of accounts waiting for whoever operates the instance.
class const SettingsButton({super.key}) extends StatelessWidget {
@override
Widget build(BuildContext context) => IconButton(
tooltip: AppLocalizations.of(context).settingsTitle,
icon: const WaitingAccountsBadge(child: Icon(LucideIcons.settings)),
onPressed: () => context.push(TrainPaths.settings),
);
}
/// Settings, over the tabs (ADR 0030, D60), grouped in cards as Learn's:
/// the operator's card for whoever operates the instance; the training's
/// screens (the body's figures, Health Connect, the Polar Loop, music,
/// those the device has); the account, near the foot (halkyon_app's
/// sections: whose it is on which instance, switching, signing out and
/// deleting it, SimpleLogin, its password and authenticators) in Train's
/// words where only Train can say it; About Halkyon Train last; and at the
/// very foot which release this is.
class const SettingsScreen({super.key}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context);
final train = PersonScope.of<TrainPerson>(context);
Widget heading(String text) => Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
child: Text(text, style: theme.textTheme.titleMedium),
);
return TopGradient(
background: theme.colorScheme.surfaceContainerLow,
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
systemOverlayStyle: TopGradient.overlayStyle(context),
title: Text(l10n.settingsTitle),
),
body: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: readableContentMaxWidth,
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Accounts waiting for approval, for whoever operates
// the instance alone (Halkyon Learn's ADR 0021, D34),
// first, as the cog's count leads here.
const InstanceOperatorCard(below: 12),
SectionCard(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
heading(l10n.settingsTraining),
// The figures of the person's body (D33).
ListTile(
leading: const Icon(LucideIcons.personStanding),
title: Text(l10n.profileTitle),
subtitle: Text(l10n.profileHint),
onTap: () => openProfileScreen(context),
),
// Health Connect on this phone (D31).
if (train.health != null)
ListTile(
leading: const Icon(LucideIcons.heartPulse),
title: Text(l10n.healthTitle),
subtitle: Text(l10n.healthHint),
onTap: () => openHealthScreen(context),
),
// The person's Polar Loop on this phone (D34).
if (train.loop != null)
ListTile(
leading: const Icon(LucideIcons.watch),
title: Text(l10n.loopTitle),
subtitle: Text(l10n.loopHint),
onTap: () => openLoopScreen(context),
),
// Music from Jellyfin on this phone (D47).
if (train.music != null)
ListTile(
leading: const Icon(LucideIcons.music),
title: Text(l10n.musicTitle),
subtitle: Text(l10n.musicHint),
onTap: () => openMusicScreen(context),
),
],
),
),
const SizedBox(height: 12),
SectionCard(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
heading(l10n.accountTitle),
AccountTiles(deleteMessage: l10n.accountDeleteMessage),
// SimpleLogin keeps the account where the instance
// offers it (ADR 0020, D21), and a password and an
// authenticator, signing in on other devices and in
// the instance's other apps (ADR 0017).
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SimpleLoginSection(),
AccountSecuritySection(),
],
),
),
],
),
),
const SizedBox(height: 12),
SectionCard(
padding: const EdgeInsets.symmetric(vertical: 8),
child: ListTile(
leading: const Icon(LucideIcons.info),
title: Text(l10n.aboutTitle),
// halkyon_app's About: the app's name and icon, the
// source of what the instance runs, which both
// licences ask to offer, and the licence list.
onTap: () => showAppAbout(
context,
icon: const Image(
image: trainIcon,
width: 48,
height: 48,
),
),
),
),
const SizedBox(height: 24),
const _Version(),
],
),
),
),
),
),
);
}
}
/// Which release of the app this is, so that a report can name it.
class const _Version() extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final (:version, :build) = TrainDependencies.of(context).appVersion;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
AppLocalizations.of(context).settingsVersion(version, '${build ?? ''}'),
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
);
}
}

View file

@ -2,13 +2,14 @@ import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
import 'package:halkyon_train_flutter/src/feature/workout/widget/workouts_list.dart';
import 'package:halkyon_train_flutter/src/settings/settings_screen.dart';
import 'package:halkyon_train_flutter/src/tabs/tab_screen.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// Home (ADR 0030, D60): the person's workouts (D24), a reset of their keys
/// waiting above them (`KeysResetBanner`).
/// waiting above them (`KeysResetBanner`); the cog to Settings.
class const HomeTab({super.key}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
@ -16,6 +17,7 @@ class const HomeTab({super.key}) extends StatelessWidget {
final records = PersonScope.of<TrainPerson>(context).recording != null;
return TabScreen(
title: l10n.shellTabHome,
actions: const [SettingsButton()],
body: Column(
children: [
const KeysResetBanner(),

View file

@ -1,32 +1,25 @@
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
import 'package:halkyon_train_flutter/src/account/account_sheet.dart';
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
import 'package:halkyon_train_flutter/src/feature/day/widget/days_list.dart';
import 'package:halkyon_train_flutter/src/feature/plan/widget/plans_list.dart';
import 'package:halkyon_train_flutter/src/settings/settings_screen.dart';
import 'package:halkyon_train_flutter/src/tabs/tab_screen.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// Profile (ADR 0030, D60): the person's days from the Loop (D37), and the
/// way to their account. Where the device doesn't record, and so has no
/// cog to Settings. Where the device doesn't record, and so has no
/// Record tab, the interval plans (D42) are here beside the days.
class const ProfileTab({super.key}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final records = PersonScope.of<TrainPerson>(context).recording != null;
final account = IconButton(
tooltip: l10n.accountTitle,
icon: const WaitingAccountsBadge(
child: Icon(LucideIcons.circleUserRound),
),
onPressed: () => showAccountSheet(context),
);
const settings = SettingsButton();
if (records) {
return TabScreen(
title: l10n.shellTabProfile,
actions: [account],
actions: const [settings],
body: const DaysList(),
);
}
@ -34,7 +27,7 @@ class const ProfileTab({super.key}) extends StatelessWidget {
length: 2,
child: TabScreen(
title: l10n.shellTabProfile,
actions: [account],
actions: const [settings],
bottom: TabBar(
tabs: [
Tab(text: l10n.homeDays),

View file

@ -151,6 +151,7 @@ void main() {
FakeInstanceOperatorGateway(),
),
fileSaver: MemoryFileSaver(),
appVersion: (version: '0.1.0', build: 7),
sourceCodeUrl: () async =>
'https://code.halkyon.app/project-halkyon/halkyon',
openPerson: (person) async {
@ -248,10 +249,9 @@ void main() {
matching: find.text(label),
);
/// Opens the account sheet from Profile.
Future<void> openAccount(WidgetTester tester) async {
await goTab(tester, 'Profile');
await tester.tap(find.byTooltip('Account'));
/// Opens Settings from Home's cog.
Future<void> openSettings(WidgetTester tester) async {
await tester.tap(find.byTooltip('Settings'));
await tester.pumpAndSettle();
}
@ -379,6 +379,38 @@ void main() {
expect(find.text('New route'), findsOneWidget);
});
testWidgets("Home's and Profile's cog leads to Settings, as Learn's", (
tester,
) async {
await start(tester, usedBefore: true);
for (final tab in ['Home', 'Profile']) {
await goTab(tester, tab);
expect(find.byTooltip('Account'), findsNothing);
await openSettings(tester);
expect(find.text('Settings'), findsOneWidget, reason: tab);
await tester.pageBack();
await tester.pumpAndSettle();
}
});
testWidgets('Settings groups the training first, then the account, About '
'Halkyon Train last and the version small at the very foot', (
tester,
) async {
await start(tester, usedBefore: true);
await openSettings(tester);
final order = [
'Your body',
'Account',
'Sign out',
'About Halkyon Train',
'Version 0.1.0 (build 7)',
].map((text) => tester.getTopLeft(find.text(text)).dy).toList();
expect(order, [...order]..sort());
});
testWidgets("from medium windows on, a rail with Train's icon on top", (
tester,
) async {
@ -419,7 +451,7 @@ void main() {
testWidgets('the account says on which instance it is signed in; signing '
'out deletes nothing and asks how to start', (tester) async {
await start(tester, usedBefore: true);
await openAccount(tester);
await openSettings(tester);
expect(find.text('Signed in on train.example.org'), findsOneWidget);
expect(find.text('Switch account'), findsOneWidget);
@ -441,14 +473,14 @@ void main() {
simpleLogin: FakeSimpleLoginGateway(gateway),
);
await openAccount(tester);
await openSettings(tester);
expect(find.widgetWithText(ListTile, 'Add SimpleLogin'), findsOneWidget);
});
testWidgets('deleting the account asks first; Cancel keeps it, Delete '
'deletes it and asks how to start', (tester) async {
await start(tester, usedBefore: true);
await openAccount(tester);
await openSettings(tester);
await tester.tap(find.text('Delete account'));
await tester.pumpAndSettle();
@ -466,10 +498,10 @@ void main() {
expect(find.text('Start on train.example.org:'), findsOneWidget);
});
testWidgets('a deletion the instance never got says so on the account '
'sheet, the person still signed in', (tester) async {
testWidgets('a deletion the instance never got says so in Settings, the '
'person still signed in', (tester) async {
await start(tester, usedBefore: true);
await openAccount(tester);
await openSettings(tester);
gateway.deletionError = Exception('No connection');
await tester.tap(find.text('Delete account'));
@ -478,16 +510,12 @@ void main() {
await tester.pumpAndSettle();
expect(
find.descendant(
of: find.byType(SheetScaffold),
matching: find.widgetWithText(
SnackBar,
"Couldn't delete your account: the instance wasn't reached. "
'Try again.',
),
find.widgetWithText(
SnackBar,
"Couldn't delete your account: the instance wasn't reached. "
'Try again.',
),
findsOneWidget,
reason: 'on the sheet, not behind its barrier',
);
expect(gateway.status, isA<SignedIn>());
});
@ -495,9 +523,9 @@ void main() {
testWidgets('About names the source of what the instance runs, which both '
'licences ask to offer', (tester) async {
await start(tester, usedBefore: true);
await openAccount(tester);
await openSettings(tester);
await tester.tap(find.text('About'));
await tester.tap(find.text('About Halkyon Train'));
await tester.pumpAndSettle();
expect(find.text('The source of what this instance runs:'), findsOneWidget);
@ -511,7 +539,7 @@ void main() {
tester,
) async {
await start(tester, usedBefore: true);
await openAccount(tester);
await openSettings(tester);
expect(find.text('Health Connect'), findsNothing);
});
@ -537,7 +565,7 @@ void main() {
Future<void> openBody(WidgetTester tester) async {
await start(tester, usedBefore: true, profile: profile);
await openAccount(tester);
await openSettings(tester);
await tester.tap(find.text('Your body'));
await tester.pumpAndSettle();
}
@ -701,7 +729,7 @@ void main() {
loop: loop,
);
await tester.runAsync(() async => loop.start());
await openAccount(tester);
await openSettings(tester);
await tester.tap(find.text('Polar Loop'));
await frames(tester);
}
@ -715,7 +743,7 @@ void main() {
testWidgets('a phone that holds no Loop offers none', (tester) async {
await start(tester, usedBefore: true);
await openAccount(tester);
await openSettings(tester);
expect(find.text('Polar Loop'), findsNothing);
});
@ -952,7 +980,7 @@ void main() {
workouts,
)..start(),
);
await openAccount(tester);
await openSettings(tester);
await tester.tap(find.text('Health Connect'));
await tester.pumpAndSettle();
}
@ -1092,8 +1120,8 @@ void main() {
await tester.pumpAndSettle();
await tester.pageBack();
await tester.pumpAndSettle();
// The account sheet, closed.
await tester.tapAt(const Offset(200, 50));
// Settings, left.
await tester.pageBack();
await tester.pumpAndSettle();
// Home, where the workouts are.
await goTab(tester, 'Home');
@ -1114,7 +1142,7 @@ void main() {
await tester.pumpAndSettle();
await tester.pageBack();
await tester.pumpAndSettle();
await tester.tapAt(const Offset(200, 50));
await tester.pageBack();
await tester.pumpAndSettle();
// Home, where the workouts are.
await goTab(tester, 'Home');
@ -1148,7 +1176,7 @@ void main() {
await tester.pumpAndSettle();
await tester.pageBack();
await tester.pumpAndSettle();
await tester.tapAt(const Offset(200, 50));
await tester.pageBack();
await tester.pumpAndSettle();
// Home, where the workouts are.
await goTab(tester, 'Home');

View file

@ -1487,6 +1487,10 @@ Strava. What was found, 27 Oct:
Train's icon, as Learn's in cobalt (`TopGradient` under the app's `ScreenColor`);
buttons and links stay cobalt. Over cobalt (Learn's, so the two apps would look
alike), strawberry and the icon's whole sweep.
Settings is a screen over the tabs, as Learn's, from a cog atop Home and Profile
(31 Oct, the developer's), where an account sheet had held the account and the
training's screens: grouped in cards, the training's screens first, the account near
the foot, About Halkyon Train last and the version small below it.
## Consequences