mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 19:27:16 +01:00
sync_settings_ui: split sync_settings_screen.dart 382 -> 192
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
This commit is contained in:
parent
ea0dc9348c
commit
06ca32ce6d
@ -16,6 +16,12 @@ analyzer:
|
||||
- "**/*.g.dart"
|
||||
- "**/*.freezed.dart"
|
||||
- "test/**"
|
||||
- android/**
|
||||
- ios/**
|
||||
- web/**
|
||||
- windows/**
|
||||
- macos/**
|
||||
- linux/**
|
||||
|
||||
linter:
|
||||
rules:
|
||||
|
||||
@ -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<FirebaseAccount?> Function() accountLoader;
|
||||
|
||||
/// Persists the account. See [accountLoader].
|
||||
final Future<void> Function(FirebaseAccount) accountSaver;
|
||||
|
||||
/// Forgets the account and any cached session. See [accountLoader].
|
||||
final Future<void> 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<bool> Function() sessionProbe;
|
||||
|
||||
/// Builds the Firebase backend from the stored account. Injected so tests
|
||||
/// can supply a fake.
|
||||
final Future<FirebaseRestClient?> 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<FirebaseRestClient?> 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<SyncSettingsScreen> createState() => _SyncSettingsScreenState();
|
||||
}
|
||||
part 'sync_settings_sections.dart';
|
||||
part 'sync_settings_widget.dart';
|
||||
|
||||
class _SyncSettingsScreenState extends State<SyncSettingsScreen> {
|
||||
late final FirebaseSyncController _firebase = FirebaseSyncController(
|
||||
@ -254,129 +190,3 @@ class _SyncSettingsScreenState extends State<SyncSettingsScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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}'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
132
sync_settings_ui/lib/src/sync_settings_sections.dart
Normal file
132
sync_settings_ui/lib/src/sync_settings_sections.dart
Normal file
@ -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}'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
68
sync_settings_ui/lib/src/sync_settings_widget.dart
Normal file
68
sync_settings_ui/lib/src/sync_settings_widget.dart
Normal file
@ -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<FirebaseAccount?> Function() accountLoader;
|
||||
|
||||
/// Persists the account. See [accountLoader].
|
||||
final Future<void> Function(FirebaseAccount) accountSaver;
|
||||
|
||||
/// Forgets the account and any cached session. See [accountLoader].
|
||||
final Future<void> 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<bool> Function() sessionProbe;
|
||||
|
||||
/// Builds the Firebase backend from the stored account. Injected so tests
|
||||
/// can supply a fake.
|
||||
final Future<FirebaseRestClient?> 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<FirebaseRestClient?> 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<SyncSettingsScreen> createState() => _SyncSettingsScreenState();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user