feat(train): where sync stands in Settings, with Sync now, and the cog's dot

As Learn's and Todo's: halkyon_app's Sync card first in Settings, and the
dot on the cog while sync fails; the person's SyncService is now part of
TrainPerson, which was the only reason Train had neither (the developer's
review of 3 Nov 2026).

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 21:31:45 +01:00
commit adbb2c4f2f
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 29 additions and 6 deletions

View file

@ -106,6 +106,12 @@ class _TrainPersonAppState() extends State<TrainPersonApp> {
create: (context) => create: (context) =>
OperatingCubit(AppScope.of(context).instanceOperators), OperatingCubit(AppScope.of(context).instanceOperators),
), ),
// Where sync stands: Settings' card and the dot on the cog, as
// Learn's and Todo's.
BlocProvider(
create: (context) =>
SyncStatusBloc(PersonScope.of<TrainPerson>(context).sync),
),
BlocProvider( BlocProvider(
create: (context) { create: (context) {
final train = PersonScope.of<TrainPerson>(context); final train = PersonScope.of<TrainPerson>(context);

View file

@ -75,6 +75,10 @@ class TrainPerson(
/// Whose they are. /// Whose they are.
final SignedIn person, { final SignedIn person, {
/// What sends their changes and brings their other devices' (D14):
/// Settings' Sync card and the cog's dot follow it.
required final SyncService sync,
/// Their workouts, sealed and synced. /// Their workouts, sealed and synced.
required final WorkoutRepository workouts, required final WorkoutRepository workouts,
@ -496,6 +500,7 @@ Future<TrainPerson> _openPersonData(
: AppLifecycleListener(onResume: () => unawaited(keptMusic.sync())); : AppLifecycleListener(onResume: () => unawaited(keptMusic.sync()));
return TrainPerson( return TrainPerson(
person, person,
sync: sync,
workouts: workouts, workouts: workouts,
profile: profile, profile: profile,
days: days, days: days,

View file

@ -20,13 +20,16 @@ class const SettingsButton({super.key}) extends StatelessWidget {
@override @override
Widget build(BuildContext context) => IconButton( Widget build(BuildContext context) => IconButton(
tooltip: AppLocalizations.of(context).settingsTitle, tooltip: AppLocalizations.of(context).settingsTitle,
icon: const WaitingAccountsBadge(child: Icon(LucideIcons.settings)), // The dot of a failing sync, which the Sync card explains, as Learn's.
icon: const SyncTroubleBadge(
child: WaitingAccountsBadge(child: Icon(LucideIcons.settings)),
),
onPressed: () => context.push(TrainPaths.settings), onPressed: () => context.push(TrainPaths.settings),
); );
} }
/// Settings, over the tabs (ADR 0030, D60), grouped in cards as Learn's: /// 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 /// where sync stands, with Sync now; the operator's card for whoever operates the instance; the training's
/// screens (the body's figures, Health Connect, the Polar Loop, music, /// screens (the body's figures, Health Connect, the Polar Loop, music,
/// those the device has); the account, near the foot (halkyon_app's /// those the device has); the account, near the foot (halkyon_app's
/// sections: whose it is on which instance, switching, signing out and /// sections: whose it is on which instance, switching, signing out and
@ -63,6 +66,10 @@ class const SettingsScreen({super.key}) extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Where sync stands, first, as Learn's and Todo's: the
// cog's dot leads here.
const SyncStatusCard(),
const SizedBox(height: 12),
// Accounts waiting for approval, for whoever operates // Accounts waiting for approval, for whoever operates
// the instance alone (Halkyon Learn's ADR 0021, D34), // the instance alone (Halkyon Learn's ADR 0021, D34),
// first, as the cog's count leads here. // first, as the cog's count leads here.

View file

@ -191,6 +191,10 @@ void main() {
opened.add(person); opened.add(person);
return TrainPerson( return TrainPerson(
person, person,
sync: SyncService(
({required onMerge}) async {},
debounce: const Duration(days: 1),
),
workouts: workouts:
workouts ?? workouts ??
WorkoutRepository( WorkoutRepository(
@ -488,14 +492,15 @@ void main() {
} }
}); });
testWidgets('Settings groups the training first, then the account, About ' testWidgets('Settings groups where sync stands first, with Sync now, then '
'Halkyon Train last and the version small at the very foot', ( 'the training, then the account, About Halkyon Train last and the '
tester, 'version small at the very foot', (tester) async {
) async {
await start(tester, usedBefore: true); await start(tester, usedBefore: true);
await openSettings(tester); await openSettings(tester);
expect(find.text('Sync now'), findsOneWidget);
final order = [ final order = [
'Sync',
'Your body', 'Your body',
'Account', 'Account',
'Sign out', 'Sign out',