From 06ca32ce6df8921d32dde1f8c9582d70afa9f44e Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 22 Aug 2026 15:38:58 +0200 Subject: [PATCH] sync_settings_ui: split sync_settings_screen.dart 382 -> 192 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sync_settings_widget.dart 68 — the public StatefulWidget and its callbacks sync_settings_sections.dart 132 — the two sections the screen composes sync_settings_screen.dart 192 — the State: loading, connecting, build Uses `part`/`part of`, not imports: _FirebaseSection and _BackupSection are library-private, so a sibling file cannot import them at all — extracting them as ordinary files would not compile. Two shapes tried and rejected on the way: an `extension _BackupActions` on the State (an extension cannot call the protected setState), and moving build() out of the class (it is an @override and has to stay). Both reverted from HEAD rather than patched around. flutter analyze clean, 29 tests pass. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG --- sync_settings_ui/analysis_options.yaml | 6 + .../lib/src/sync_settings_screen.dart | 194 +----------------- .../lib/src/sync_settings_sections.dart | 132 ++++++++++++ .../lib/src/sync_settings_widget.dart | 68 ++++++ 4 files changed, 208 insertions(+), 192 deletions(-) create mode 100644 sync_settings_ui/lib/src/sync_settings_sections.dart create mode 100644 sync_settings_ui/lib/src/sync_settings_widget.dart diff --git a/sync_settings_ui/analysis_options.yaml b/sync_settings_ui/analysis_options.yaml index bac0810..286e24c 100644 --- a/sync_settings_ui/analysis_options.yaml +++ b/sync_settings_ui/analysis_options.yaml @@ -16,6 +16,12 @@ analyzer: - "**/*.g.dart" - "**/*.freezed.dart" - "test/**" + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** linter: rules: diff --git a/sync_settings_ui/lib/src/sync_settings_screen.dart b/sync_settings_ui/lib/src/sync_settings_screen.dart index 9fd3a64..6f7c22d 100644 --- a/sync_settings_ui/lib/src/sync_settings_screen.dart +++ b/sync_settings_ui/lib/src/sync_settings_screen.dart @@ -5,72 +5,8 @@ import 'package:flutter/material.dart'; import 'package:sync_settings_ui/src/backup_slot.dart'; import 'package:sync_settings_ui/src/firebase_sync_controller.dart'; -/// The shared "Sync settings" screen: Firebase sync (primary, shown by -/// default) and an optional local backup section. -/// -/// Generalized from the near-identical settings screens duplicated across -/// the todo app, diet_guard_app, workout_app, wake_alarm/phone_app and -/// home_inventory. Only the sync-related fields migrate here -- app-specific -/// settings (kcal goals, exercise thresholds, notifications, the -/// battery-exemption toggle) stay behind in each app's own Settings screen, -/// reached via a `ListTile` link to this one. -class SyncSettingsScreen extends StatefulWidget { - /// Creates a [SyncSettingsScreen]. - const SyncSettingsScreen({ - required this.accountLoader, - required this.accountSaver, - required this.accountClearer, - required this.sessionProbe, - required this.firebaseFactory, - this.googleFirebaseFactory, - this.googleAvailable, - this.backup, - super.key, - }); - - /// Reads the stored Firebase account. Injected because the keystore is a - /// platform channel `flutter test` has no binding for. - final Future Function() accountLoader; - - /// Persists the account. See [accountLoader]. - final Future Function(FirebaseAccount) accountSaver; - - /// Forgets the account and any cached session. See [accountLoader]. - final Future Function() accountClearer; - - /// Whether a Firebase session is stored. - /// - /// Separate from [accountLoader] because the two answer different - /// questions: the account marker is bookkeeping, the session is the - /// credential. A device can hold the second without the first, and - /// reporting only the first is what made a syncing phone read as - /// "not connected". - final Future Function() sessionProbe; - - /// Builds the Firebase backend from the stored account. Injected so tests - /// can supply a fake. - final Future Function() firebaseFactory; - - /// Builds the Firebase backend via Google sign-in. Separate from - /// [firebaseFactory] because it reaches the Google plugin's platform - /// channel. Null hides the "Sign in with Google" button entirely. - final Future Function()? googleFirebaseFactory; - - /// Narrows whether to offer the Google button. Can only turn it off, never - /// on: the button always requires a non-null [googleFirebaseFactory] - /// regardless of this value, so a host that sets this true without also - /// wiring the factory does not get a button that can never succeed. Tests - /// pass `false` to model a platform that reports Google sign-in - /// unsupported even though a factory is wired. - final bool? googleAvailable; - - /// Optional local export/import section. Null omits the "Backup" section - /// entirely (diet_guard, wake_alarm have no local backup format). - final BackupSlot? backup; - - @override - State createState() => _SyncSettingsScreenState(); -} +part 'sync_settings_sections.dart'; +part 'sync_settings_widget.dart'; class _SyncSettingsScreenState extends State { late final FirebaseSyncController _firebase = FirebaseSyncController( @@ -254,129 +190,3 @@ class _SyncSettingsScreenState extends State { ); } } - -class _FirebaseSection extends StatelessWidget { - const _FirebaseSection({ - required this.connected, - required this.busy, - required this.googleAvailable, - required this.emailController, - required this.passwordController, - required this.onConnectPassword, - required this.onConnectGoogle, - required this.onDisconnect, - }); - - final bool connected; - final bool busy; - final bool googleAvailable; - final TextEditingController emailController; - final TextEditingController passwordController; - final VoidCallback onConnectPassword; - final VoidCallback onConnectGoogle; - final VoidCallback onDisconnect; - - @override - Widget build(BuildContext context) { - if (connected) { - return Row( - children: [ - const Icon(Icons.cloud_done, size: 20), - const SizedBox(width: 8), - Expanded(child: Text(emailController.text)), - TextButton( - onPressed: busy ? null : onDisconnect, - child: const Text('Disconnect'), - ), - ], - ); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (googleAvailable) ...[ - Align( - alignment: Alignment.centerLeft, - child: FilledButton.icon( - onPressed: busy ? null : onConnectGoogle, - icon: const Icon(Icons.account_circle), - label: const Text('Sign in with Google'), - ), - ), - const SizedBox(height: 12), - ], - ExpansionTile( - initiallyExpanded: !googleAvailable, - title: const Text('Use the account password instead'), - tilePadding: EdgeInsets.zero, - childrenPadding: const EdgeInsets.only(bottom: 8), - children: [ - TextField( - controller: emailController, - keyboardType: TextInputType.emailAddress, - autocorrect: false, - decoration: const InputDecoration( - labelText: 'Sync account email', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 12), - TextField( - controller: passwordController, - obscureText: true, - autocorrect: false, - decoration: const InputDecoration( - labelText: 'Sync account password', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 12), - Align( - alignment: Alignment.centerLeft, - child: FilledButton.icon( - onPressed: busy ? null : onConnectPassword, - icon: const Icon(Icons.cloud_done), - label: const Text('Connect Firebase'), - ), - ), - ], - ), - ], - ); - } -} - -class _BackupSection extends StatelessWidget { - const _BackupSection({ - required this.backup, - required this.onExport, - required this.onImport, - }); - - final BackupSlot backup; - final VoidCallback onExport; - final VoidCallback onImport; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: onExport, - icon: const Icon(Icons.upload), - label: Text('Export ${backup.label}'), - ), - ), - const SizedBox(width: 12), - Expanded( - child: OutlinedButton.icon( - onPressed: onImport, - icon: const Icon(Icons.download), - label: Text('Import ${backup.label}'), - ), - ), - ], - ); - } -} diff --git a/sync_settings_ui/lib/src/sync_settings_sections.dart b/sync_settings_ui/lib/src/sync_settings_sections.dart new file mode 100644 index 0000000..cb00fda --- /dev/null +++ b/sync_settings_ui/lib/src/sync_settings_sections.dart @@ -0,0 +1,132 @@ +part of 'sync_settings_screen.dart'; + +/// The two sections the screen composes. +/// +/// Split from sync_settings_screen.dart for the 250-line cap; +/// a `part` rather than an import because both sections are +/// library-private and could not be imported at all. +class _FirebaseSection extends StatelessWidget { + const _FirebaseSection({ + required this.connected, + required this.busy, + required this.googleAvailable, + required this.emailController, + required this.passwordController, + required this.onConnectPassword, + required this.onConnectGoogle, + required this.onDisconnect, + }); + + final bool connected; + final bool busy; + final bool googleAvailable; + final TextEditingController emailController; + final TextEditingController passwordController; + final VoidCallback onConnectPassword; + final VoidCallback onConnectGoogle; + final VoidCallback onDisconnect; + + @override + Widget build(BuildContext context) { + if (connected) { + return Row( + children: [ + const Icon(Icons.cloud_done, size: 20), + const SizedBox(width: 8), + Expanded(child: Text(emailController.text)), + TextButton( + onPressed: busy ? null : onDisconnect, + child: const Text('Disconnect'), + ), + ], + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (googleAvailable) ...[ + Align( + alignment: Alignment.centerLeft, + child: FilledButton.icon( + onPressed: busy ? null : onConnectGoogle, + icon: const Icon(Icons.account_circle), + label: const Text('Sign in with Google'), + ), + ), + const SizedBox(height: 12), + ], + ExpansionTile( + initiallyExpanded: !googleAvailable, + title: const Text('Use the account password instead'), + tilePadding: EdgeInsets.zero, + childrenPadding: const EdgeInsets.only(bottom: 8), + children: [ + TextField( + controller: emailController, + keyboardType: TextInputType.emailAddress, + autocorrect: false, + decoration: const InputDecoration( + labelText: 'Sync account email', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextField( + controller: passwordController, + obscureText: true, + autocorrect: false, + decoration: const InputDecoration( + labelText: 'Sync account password', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerLeft, + child: FilledButton.icon( + onPressed: busy ? null : onConnectPassword, + icon: const Icon(Icons.cloud_done), + label: const Text('Connect Firebase'), + ), + ), + ], + ), + ], + ); + } +} + +class _BackupSection extends StatelessWidget { + const _BackupSection({ + required this.backup, + required this.onExport, + required this.onImport, + }); + + final BackupSlot backup; + final VoidCallback onExport; + final VoidCallback onImport; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: onExport, + icon: const Icon(Icons.upload), + label: Text('Export ${backup.label}'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: OutlinedButton.icon( + onPressed: onImport, + icon: const Icon(Icons.download), + label: Text('Import ${backup.label}'), + ), + ), + ], + ); + } +} diff --git a/sync_settings_ui/lib/src/sync_settings_widget.dart b/sync_settings_ui/lib/src/sync_settings_widget.dart new file mode 100644 index 0000000..0bea519 --- /dev/null +++ b/sync_settings_ui/lib/src/sync_settings_widget.dart @@ -0,0 +1,68 @@ +part of 'sync_settings_screen.dart'; + +/// The shared "Sync settings" screen: Firebase sync (primary, shown by +/// default) and an optional local backup section. +/// +/// Generalized from the near-identical settings screens duplicated across +/// the todo app, diet_guard_app, workout_app, wake_alarm/phone_app and +/// home_inventory. Only the sync-related fields migrate here -- app-specific +/// settings (kcal goals, exercise thresholds, notifications, the +/// battery-exemption toggle) stay behind in each app's own Settings screen, +/// reached via a `ListTile` link to this one. +class SyncSettingsScreen extends StatefulWidget { + /// Creates a [SyncSettingsScreen]. + const SyncSettingsScreen({ + required this.accountLoader, + required this.accountSaver, + required this.accountClearer, + required this.sessionProbe, + required this.firebaseFactory, + this.googleFirebaseFactory, + this.googleAvailable, + this.backup, + super.key, + }); + + /// Reads the stored Firebase account. Injected because the keystore is a + /// platform channel `flutter test` has no binding for. + final Future Function() accountLoader; + + /// Persists the account. See [accountLoader]. + final Future Function(FirebaseAccount) accountSaver; + + /// Forgets the account and any cached session. See [accountLoader]. + final Future Function() accountClearer; + + /// Whether a Firebase session is stored. + /// + /// Separate from [accountLoader] because the two answer different + /// questions: the account marker is bookkeeping, the session is the + /// credential. A device can hold the second without the first, and + /// reporting only the first is what made a syncing phone read as + /// "not connected". + final Future Function() sessionProbe; + + /// Builds the Firebase backend from the stored account. Injected so tests + /// can supply a fake. + final Future Function() firebaseFactory; + + /// Builds the Firebase backend via Google sign-in. Separate from + /// [firebaseFactory] because it reaches the Google plugin's platform + /// channel. Null hides the "Sign in with Google" button entirely. + final Future Function()? googleFirebaseFactory; + + /// Narrows whether to offer the Google button. Can only turn it off, never + /// on: the button always requires a non-null [googleFirebaseFactory] + /// regardless of this value, so a host that sets this true without also + /// wiring the factory does not get a button that can never succeed. Tests + /// pass `false` to model a platform that reports Google sign-in + /// unsupported even though a factory is wired. + final bool? googleAvailable; + + /// Optional local export/import section. Null omits the "Backup" section + /// entirely (diet_guard, wake_alarm have no local backup format). + final BackupSlot? backup; + + @override + State createState() => _SyncSettingsScreenState(); +}